using Xunit; using Semmle.Autobuild.Shared; using Semmle.Util; using System.Collections.Generic; using System; using System.Linq; using Microsoft.Build.Construction; using System.Xml; using System.IO; namespace Semmle.Autobuild.Cpp.Tests { /// /// Test class to script Autobuilder scenarios. /// For most methods, it uses two fields: /// - an IList to capture the the arguments passed to it /// - an IDictionary of possible return values. /// class TestActions : IBuildActions { /// /// List of strings passed to FileDelete. /// public IList FileDeleteIn = new List(); void IBuildActions.FileDelete(string file) { FileDeleteIn.Add(file); } public IList FileExistsIn = new List(); public IDictionary FileExists = new Dictionary(); bool IBuildActions.FileExists(string file) { FileExistsIn.Add(file); if (FileExists.TryGetValue(file, out var ret)) return ret; if (FileExists.TryGetValue(System.IO.Path.GetFileName(file), out ret)) return ret; throw new ArgumentException("Missing FileExists " + file); } public IList RunProcessIn = new List(); public IDictionary RunProcess = new Dictionary(); public IDictionary RunProcessOut = new Dictionary(); public IDictionary RunProcessWorkingDirectory = new Dictionary(); public HashSet CreateDirectories { get; } = new HashSet(); public HashSet<(string, string)> DownloadFiles { get; } = new HashSet<(string, string)>(); int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? env, out IList stdOut) { var pattern = cmd + " " + args; RunProcessIn.Add(pattern); if (RunProcessOut.TryGetValue(pattern, out var str)) stdOut = str.Split("\n"); else throw new ArgumentException("Missing RunProcessOut " + pattern); RunProcessWorkingDirectory.TryGetValue(pattern, out var wd); if (wd != workingDirectory) throw new ArgumentException("Missing RunProcessWorkingDirectory " + pattern); if (RunProcess.TryGetValue(pattern, out var ret)) return ret; throw new ArgumentException("Missing RunProcess " + pattern); } int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? env) { var pattern = cmd + " " + args; RunProcessIn.Add(pattern); RunProcessWorkingDirectory.TryGetValue(pattern, out var wd); if (wd != workingDirectory) throw new ArgumentException("Missing RunProcessWorkingDirectory " + pattern); if (RunProcess.TryGetValue(pattern, out var ret)) return ret; throw new ArgumentException("Missing RunProcess " + pattern); } int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? env, BuildOutputHandler onOutput, BuildOutputHandler onError) { var ret = (this as IBuildActions).RunProcess(cmd, args, workingDirectory, env, out var stdout); stdout.ForEach(line => onOutput(line)); return ret; } public IList DirectoryDeleteIn = new List(); void IBuildActions.DirectoryDelete(string dir, bool recursive) { DirectoryDeleteIn.Add(dir); } public IDictionary DirectoryExists = new Dictionary(); public IList DirectoryExistsIn = new List(); bool IBuildActions.DirectoryExists(string dir) { DirectoryExistsIn.Add(dir); if (DirectoryExists.TryGetValue(dir, out var ret)) return ret; throw new ArgumentException("Missing DirectoryExists " + dir); } public IDictionary GetEnvironmentVariable = new Dictionary(); string? IBuildActions.GetEnvironmentVariable(string name) { if (GetEnvironmentVariable.TryGetValue(name, out var ret)) return ret; throw new ArgumentException("Missing GetEnvironmentVariable " + name); } public string GetCurrentDirectory = ""; string IBuildActions.GetCurrentDirectory() { return GetCurrentDirectory; } public IDictionary EnumerateFiles = new Dictionary(); IEnumerable IBuildActions.EnumerateFiles(string dir) { if (EnumerateFiles.TryGetValue(dir, out var str)) return str.Split("\n"); throw new ArgumentException("Missing EnumerateFiles " + dir); } public IDictionary EnumerateDirectories = new Dictionary(); IEnumerable IBuildActions.EnumerateDirectories(string dir) { if (EnumerateDirectories.TryGetValue(dir, out var str)) return string.IsNullOrEmpty(str) ? Enumerable.Empty() : str.Split("\n"); throw new ArgumentException("Missing EnumerateDirectories " + dir); } public bool IsWindows; bool IBuildActions.IsWindows() => IsWindows; public bool IsMacOs { get; set; } bool IBuildActions.IsMacOs() => IsMacOs; public bool IsRunningOnAppleSilicon { get; set; } bool IBuildActions.IsRunningOnAppleSilicon() => IsRunningOnAppleSilicon; string IBuildActions.PathCombine(params string[] parts) { return string.Join(IsWindows ? '\\' : '/', parts.Where(p => !string.IsNullOrWhiteSpace(p))); } string IBuildActions.GetFullPath(string path) => path; string? IBuildActions.GetFileName(string? path) => Path.GetFileName(path?.Replace('\\', '/')); public string? GetDirectoryName(string? path) { var dir = Path.GetDirectoryName(path?.Replace('\\', '/')); return dir is null ? path : path?.Substring(0, dir.Length); } void IBuildActions.WriteAllText(string filename, string contents) { } public IDictionary LoadXml = new Dictionary(); XmlDocument IBuildActions.LoadXml(string filename) { if (LoadXml.TryGetValue(filename, out var xml)) return xml; throw new ArgumentException("Missing LoadXml " + filename); } public string EnvironmentExpandEnvironmentVariables(string s) { foreach (var kvp in GetEnvironmentVariable) s = s.Replace($"%{kvp.Key}%", kvp.Value); return s; } public void CreateDirectory(string path) { if (!CreateDirectories.Contains(path)) throw new ArgumentException($"Missing CreateDirectory, {path}"); } public void DownloadFile(string address, string fileName) { if (!DownloadFiles.Contains((address, fileName))) throw new ArgumentException($"Missing DownloadFile, {address}, {fileName}"); } public IDiagnosticsWriter CreateDiagnosticsWriter(string filename) => new TestDiagnosticWriter(); } internal class TestDiagnosticWriter : IDiagnosticsWriter { public IList Diagnostics { get; } = new List(); public void AddEntry(DiagnosticMessage message) => this.Diagnostics.Add(message); public void Dispose() { } } /// /// A fake solution to build. /// class TestSolution : ISolution { public IEnumerable Configurations => throw new NotImplementedException(); public string DefaultConfigurationName => "Release"; public string DefaultPlatformName => "x86"; public string FullPath { get; set; } public Version ToolsVersion => new Version("14.0"); public IEnumerable IncludedProjects => throw new NotImplementedException(); public TestSolution(string path) { FullPath = path; } } public class BuildScriptTests { TestActions Actions = new TestActions(); // Records the arguments passed to StartCallback. IList StartCallbackIn = new List(); void StartCallback(string s, bool silent) { StartCallbackIn.Add(s); } // Records the arguments passed to EndCallback IList EndCallbackIn = new List(); IList EndCallbackReturn = new List(); void EndCallback(int ret, string s, bool silent) { EndCallbackReturn.Add(ret); EndCallbackIn.Add(s); } CppAutobuilder CreateAutoBuilder(bool isWindows, string? dotnetVersion = null, string cwd = @"C:\Project") { string codeqlUpperLanguage = Language.Cpp.UpperCaseName; Actions.GetEnvironmentVariable[$"CODEQL_AUTOBUILDER_{codeqlUpperLanguage}_NO_INDEXING"] = "false"; Actions.GetEnvironmentVariable[$"CODEQL_EXTRACTOR_{codeqlUpperLanguage}_TRAP_DIR"] = ""; Actions.GetEnvironmentVariable[$"CODEQL_EXTRACTOR_{codeqlUpperLanguage}_SOURCE_ARCHIVE_DIR"] = ""; Actions.GetEnvironmentVariable[$"CODEQL_EXTRACTOR_{codeqlUpperLanguage}_ROOT"] = $@"C:\codeql\{codeqlUpperLanguage.ToLowerInvariant()}"; Actions.GetEnvironmentVariable[$"CODEQL_EXTRACTOR_{codeqlUpperLanguage}_DIAGNOSTIC_DIR"] = ""; Actions.GetEnvironmentVariable["CODEQL_JAVA_HOME"] = @"C:\codeql\tools\java"; Actions.GetEnvironmentVariable["CODEQL_PLATFORM"] = "win64"; Actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_OPTION_DOTNET_VERSION"] = dotnetVersion; Actions.GetEnvironmentVariable["ProgramFiles(x86)"] = isWindows ? @"C:\Program Files (x86)" : null; Actions.GetCurrentDirectory = cwd; Actions.IsWindows = isWindows; var options = new CppAutobuildOptions(Actions); return new CppAutobuilder(Actions, options); } void TestAutobuilderScript(CppAutobuilder autobuilder, int expectedOutput, int commandsRun) { Assert.Equal(expectedOutput, autobuilder.GetBuildScript().Run(Actions, StartCallback, EndCallback)); // Check expected commands actually ran Assert.Equal(commandsRun, StartCallbackIn.Count); Assert.Equal(commandsRun, EndCallbackIn.Count); Assert.Equal(commandsRun, EndCallbackReturn.Count); var action = Actions.RunProcess.GetEnumerator(); for (int cmd = 0; cmd < commandsRun; ++cmd) { Assert.True(action.MoveNext()); Assert.Equal(action.Current.Key, StartCallbackIn[cmd]); Assert.Equal(action.Current.Value, EndCallbackReturn[cmd]); } } [Fact] public void TestDefaultCppAutobuilder() { Actions.EnumerateFiles[@"C:\Project"] = ""; Actions.EnumerateDirectories[@"C:\Project"] = ""; var autobuilder = CreateAutoBuilder(true); var script = autobuilder.GetBuildScript(); // Fails due to no solutions present. Assert.NotEqual(0, script.Run(Actions, StartCallback, EndCallback)); } [Fact] public void TestCppAutobuilderSuccess() { Actions.RunProcess[@"cmd.exe /C nuget restore C:\Project\test.sln -DisableParallelProcessing"] = 1; Actions.RunProcess[@"cmd.exe /C scratch\.nuget\nuget.exe restore C:\Project\test.sln -DisableParallelProcessing"] = 0; Actions.RunProcess[@"cmd.exe /C CALL ^""C:\Program^ Files^ ^(x86^)\Microsoft^ Visual^ Studio^ 14.0\VC\vcvarsall.bat^"" && set Platform=&& type NUL && msbuild C:\Project\test.sln /t:rebuild /p:Platform=""x86"" /p:Configuration=""Release"""] = 0; Actions.RunProcessOut[@"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe -prerelease -legacy -property installationPath"] = ""; Actions.RunProcess[@"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe -prerelease -legacy -property installationPath"] = 1; Actions.RunProcess[@"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe -prerelease -legacy -property installationVersion"] = 0; Actions.RunProcessOut[@"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe -prerelease -legacy -property installationVersion"] = ""; Actions.FileExists[@"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"] = true; Actions.FileExists[@"C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat"] = true; Actions.FileExists[@"C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\vcvarsall.bat"] = true; Actions.FileExists[@"C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\vcvarsall.bat"] = true; Actions.FileExists[@"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"] = true; Actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CPP_SCRATCH_DIR"] = "scratch"; Actions.EnumerateFiles[@"C:\Project"] = "foo.cs\ntest.slx"; Actions.EnumerateDirectories[@"C:\Project"] = ""; Actions.CreateDirectories.Add(@"scratch\.nuget"); Actions.DownloadFiles.Add(("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", @"scratch\.nuget\nuget.exe")); var autobuilder = CreateAutoBuilder(true); var solution = new TestSolution(@"C:\Project\test.sln"); autobuilder.ProjectsOrSolutionsToBuild.Add(solution); TestAutobuilderScript(autobuilder, 0, 3); } } }