diff --git a/.nuspec/Xamarin.Forms.targets b/.nuspec/Xamarin.Forms.targets index 28a0db744c9..33edf05dae5 100644 --- a/.nuspec/Xamarin.Forms.targets +++ b/.nuspec/Xamarin.Forms.targets @@ -64,18 +64,23 @@ - - - <_XamlGAlreadyExecuted>true - + + + <_XamlGInputs Include="@(EmbeddedResource)" Condition="'%(Extension)' == '.xaml' AND '$(DefaultLanguageSourceExtension)' == '.cs' AND '%(TargetPath)' != ''" /> + <_XamlGOutputs Include="@(_XamlGInputs->'$(IntermediateOutputPath)%(TargetPath).g.cs')" /> + + + + - - - + XamlFiles="@(_XamlGInputs)" + OutputFiles="@(_XamlGOutputs)" + Language="$(Language)" + AssemblyName="$(AssemblyName)" /> + + + + @@ -86,10 +91,7 @@ - - - <_XamlCAlreadyExecuted>true - + + + + + @@ -115,7 +121,7 @@ Language = "$(Language)" AssemblyName = "$(AssemblyName)" OutputPath = "$(IntermediateOutputPath)"> - + diff --git a/Xamarin.Forms.Build.Tasks/XamlGTask.cs b/Xamarin.Forms.Build.Tasks/XamlGTask.cs index 97d5959921d..13b0798aaaf 100644 --- a/Xamarin.Forms.Build.Tasks/XamlGTask.cs +++ b/Xamarin.Forms.Build.Tasks/XamlGTask.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Xml; @@ -10,44 +9,45 @@ namespace Xamarin.Forms.Build.Tasks { public class XamlGTask : Task { - List _generatedCodeFiles = new List(); - [Required] public ITaskItem[] XamlFiles { get; set; } - [Output] - public ITaskItem[] GeneratedCodeFiles => _generatedCodeFiles.ToArray(); + [Required] + public ITaskItem[] OutputFiles { get; set; } public string Language { get; set; } public string AssemblyName { get; set; } - public string OutputPath { get; set; } public override bool Execute() { bool success = true; Log.LogMessage(MessageImportance.Normal, "Generating code behind for XAML files"); - if (XamlFiles == null) { + //NOTE: should not happen due to [Required], but there appears to be a place this is class is called directly + if (XamlFiles == null || OutputFiles == null) { Log.LogMessage("Skipping XamlG"); return true; } - foreach (var xamlFile in XamlFiles) { - //when invoked from `UpdateDesigntimeXaml` target, the `TargetPath` isn't set, use a random one instead - var targetPath = xamlFile.GetMetadata("TargetPath"); - if (string.IsNullOrWhiteSpace(targetPath)) - targetPath = $".{Path.GetRandomFileName()}"; + if (XamlFiles.Length != OutputFiles.Length) { + Log.LogError("\"{2}\" refers to {0} item(s), and \"{3}\" refers to {1} item(s). They must have the same number of items.", XamlFiles.Length, OutputFiles.Length, "XamlFiles", "OutputFiles"); + return false; + } - var outputFile = Path.Combine(OutputPath, $"{targetPath}.g.cs"); + for (int i = 0; i < XamlFiles.Length; i++) { + var xamlFile = XamlFiles[i]; + var outputFile = OutputFiles[i].ItemSpec; if (Path.DirectorySeparatorChar == '/' && outputFile.Contains(@"\")) outputFile = outputFile.Replace('\\','/'); else if (Path.DirectorySeparatorChar == '\\' && outputFile.Contains(@"/")) outputFile = outputFile.Replace('/', '\\'); - + var generator = new XamlGenerator(xamlFile, Language, AssemblyName, outputFile, Log); try { - if (generator.Execute()) - _generatedCodeFiles.Add(new TaskItem(Microsoft.Build.Evaluation.ProjectCollection.Escape(outputFile))); + if (!generator.Execute()) { + //If Execute() fails, the file still needs to exist because it is added to the ItemGroup + File.WriteAllText (outputFile, string.Empty); + } } catch (XmlException xe) { Log.LogError(null, null, null, xamlFile.ItemSpec, xe.LineNumber, xe.LinePosition, 0, 0, xe.Message, xe.HelpLink, xe.Source); diff --git a/Xamarin.Forms.Xaml.UnitTests/DummyBuildEngine.cs b/Xamarin.Forms.Xaml.UnitTests/MSBuild/DummyBuildEngine.cs similarity index 69% rename from Xamarin.Forms.Xaml.UnitTests/DummyBuildEngine.cs rename to Xamarin.Forms.Xaml.UnitTests/MSBuild/DummyBuildEngine.cs index df76e13a79a..9ed3119c331 100644 --- a/Xamarin.Forms.Xaml.UnitTests/DummyBuildEngine.cs +++ b/Xamarin.Forms.Xaml.UnitTests/MSBuild/DummyBuildEngine.cs @@ -1,21 +1,31 @@ using System; using System.Collections; +using System.Collections.Generic; using Microsoft.Build.Framework; namespace Xamarin.Forms.Xaml.UnitTests { public class DummyBuildEngine : IBuildEngine { + public List Errors { get; } = new List (); + + public List Warnings { get; } = new List (); + + public List Messages { get; } = new List (); + public void LogErrorEvent (BuildErrorEventArgs e) { + Errors.Add (e); } public void LogWarningEvent (BuildWarningEventArgs e) { + Warnings.Add (e); } public void LogMessageEvent (BuildMessageEventArgs e) { + Messages.Add (e); } public void LogCustomEvent (CustomBuildEventArgs e) diff --git a/Xamarin.Forms.Xaml.UnitTests/MSBuild/MSBuildTests.cs b/Xamarin.Forms.Xaml.UnitTests/MSBuild/MSBuildTests.cs new file mode 100644 index 00000000000..568f0c9b460 --- /dev/null +++ b/Xamarin.Forms.Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -0,0 +1,483 @@ +using Microsoft.Build.Locator; +using NUnit.Framework; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using static Xamarin.Forms.Xaml.UnitTests.MSBuildXmlExtensions; + +namespace Xamarin.Forms.Xaml.UnitTests +{ + //This set of tests is for validating Xamarin.Forms.targets + [TestFixture] + public class MSBuildTests + { + const string XamarinFormsTargets = "Xamarin.Forms.targets"; + + static readonly string [] references = new [] + { + "mscorlib", + "System", + "Xamarin.Forms.Core.dll", + "Xamarin.Forms.Xaml.dll", + }; + + class Xaml + { + const string XamarinFormsDefaultNamespace = "http://xamarin.com/schemas/2014/forms"; + const string XamarinFormsXNamespace = "http://schemas.microsoft.com/winfx/2006/xaml"; + + public static readonly string MainPage = $@" + + "; + + public static readonly string CustomView = $@" + + "; + } + + string testDirectory; + string tempDirectory; + string intermediateDirectory; + + [SetUp] + public void SetUp () + { + testDirectory = TestContext.CurrentContext.TestDirectory; + tempDirectory = Path.Combine (testDirectory, "temp", TestContext.CurrentContext.Test.Name); + intermediateDirectory = Path.Combine (tempDirectory, "obj", "Debug"); + Directory.CreateDirectory (tempDirectory); + + //We need to copy Xamarin.Forms.targets to the test directory, to reliably import them + var xamarinFormsTargets = Path.Combine (testDirectory, "..", "..", "..", ".nuspec", XamarinFormsTargets); + if (!File.Exists (xamarinFormsTargets)) { + //NOTE: VSTS may be running tests in a staging directory, so we can use an environment variable to find the source + // https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/build/variables?view=vsts&tabs=batch#buildsourcesdirectory + var sourcesDirectory = Environment.GetEnvironmentVariable ("BUILD_SOURCESDIRECTORY"); + if (!string.IsNullOrEmpty (sourcesDirectory)) { + xamarinFormsTargets = Path.Combine (sourcesDirectory, ".nuspec", XamarinFormsTargets); + if (!File.Exists (xamarinFormsTargets)) { + Assert.Fail ("Unable to find Xamarin.Forms.targets at path: " + xamarinFormsTargets); + } + } else { + Assert.Fail ("Unable to find Xamarin.Forms.targets at path: " + xamarinFormsTargets); + } + } + + //Copy all *.targets files to the test directory + foreach (var file in Directory.GetFiles (Path.GetDirectoryName (xamarinFormsTargets), "*.targets")) { + File.Copy (file, Path.Combine (testDirectory, Path.GetFileName (file)), true); + } + } + + [TearDown] + public void TearDown () + { + //NOTE: Windows can throw IOException: The process cannot access the file XYZ because it is being used by another process. + //A simple retry-and-give-up approach should be good enough + for (int i = 0; i < 3; i++) { + try { + if (Directory.Exists (tempDirectory)) { + Directory.Delete (tempDirectory, true); + } + break; //Success + } catch (IOException) { + System.Threading.Thread.Sleep (100); + } + } + } + + /// + /// Creates a base csproj file for these unit tests + /// + /// If true, uses a new SDK-style project + XElement NewProject (bool sdkStyle) + { + var path = Path.GetTempFileName (); + var project = NewElement ("Project"); + + var propertyGroup = NewElement ("PropertyGroup"); + if (sdkStyle) { + project.WithAttribute ("Sdk", "Microsoft.NET.Sdk"); + propertyGroup.Add (NewElement ("TargetFramework").WithValue ("netstandard2")); + //NOTE: we don't want SDK-style projects to auto-add files, tests should be able to control this + propertyGroup.Add (NewElement ("EnableDefaultCompileItems").WithValue ("False")); + //NOTE: SDK-style output paths are different + if (!intermediateDirectory.EndsWith ("netstandard2")) + intermediateDirectory = Path.Combine (intermediateDirectory, "netstandard2"); + } else { + propertyGroup.Add (NewElement ("Configuration").WithValue ("Debug")); + propertyGroup.Add (NewElement ("Platform").WithValue ("AnyCPU")); + propertyGroup.Add (NewElement ("OutputType").WithValue ("Library")); + propertyGroup.Add (NewElement ("OutputPath").WithValue ("bin\\Debug")); + propertyGroup.Add (NewElement ("TargetFrameworkVersion").WithValue ("v4.7")); + } + project.Add (propertyGroup); + + var itemGroup = NewElement ("ItemGroup"); + foreach (var assembly in references) { + var reference = NewElement ("Reference").WithAttribute ("Include", assembly); + if (assembly.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) { + reference.Add (NewElement ("HintPath").WithValue (Path.Combine ("..", "..", assembly))); + } else if (sdkStyle) { + //NOTE: SDK-style projects don't need system references + continue; + } + itemGroup.Add (reference); + } + project.Add (itemGroup); + + //Let's enable XamlC assembly-wide + project.Add (AddFile ("AssemblyInfo.cs", "Compile", "[assembly: Xamarin.Forms.Xaml.XamlCompilation (Xamarin.Forms.Xaml.XamlCompilationOptions.Compile)]")); + + if (!sdkStyle) + project.Add (NewElement ("Import").WithAttribute ("Project", @"$(MSBuildBinPath)\Microsoft.CSharp.targets")); + + //Import Xamarin.Forms.targets that was copied to the test directory in [SetUp] + project.Add (NewElement ("Import").WithAttribute ("Project", Path.Combine (testDirectory, XamarinFormsTargets))); + + return project; + } + + XElement AddFile (string name, string buildAction, string contents) + { + var filePath = Path.Combine (tempDirectory, name.Replace ('\\', Path.DirectorySeparatorChar).Replace ('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory (Path.GetDirectoryName (filePath)); + File.WriteAllText (filePath, contents); + var itemGroup = NewElement ("ItemGroup"); + itemGroup.Add (NewElement (buildAction).WithAttribute ("Include", name)); + return itemGroup; + } + + string FindMSBuild () + { + //On Windows we have to "find" MSBuild + if (Environment.OSVersion.Platform == PlatformID.Win32NT) { + foreach (var visualStudioInstance in MSBuildLocator.QueryVisualStudioInstances ().OrderByDescending (v => v.Version)) { + return Path.Combine (visualStudioInstance.MSBuildPath, "MSBuild.exe"); + } + } + + return "msbuild"; + } + + void RestoreIfNeeded (string projectFile, bool sdkStyle) + { + //If using an SDK-style project, we need to run the Restore target + if (sdkStyle) { + Build (projectFile, "Restore"); + } + } + + void Build (string projectFile, string target = "Build", string additionalArgs = "") + { + var psi = new ProcessStartInfo { + FileName = FindMSBuild (), + Arguments = $"/v:minimal /nologo {projectFile} /t:{target} /bl {additionalArgs}", + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + WorkingDirectory = tempDirectory, + }; + using (var p = new Process { StartInfo = psi }) { + p.ErrorDataReceived += (s, e) => Console.Error.WriteLine (e.Data); + p.OutputDataReceived += (s, e) => Console.WriteLine (e.Data); + + p.Start (); + p.BeginErrorReadLine (); + p.BeginOutputReadLine (); + p.WaitForExit (); + Assert.AreEqual (0, p.ExitCode, "MSBuild exited with {0}", p.ExitCode); + } + } + + void AssertExists (string path, bool nonEmpty = false) + { + if (!File.Exists (path)) + Assert.Fail ($"{path} should exist!"); + + if (nonEmpty && new FileInfo (path).Length == 0) + Assert.Fail ($"{path} is empty!"); + } + + void AssertDoesNotExist (string path) + { + if (File.Exists (path)) + Assert.Fail ($"{path} should *not* exist!"); + } + + [Test] + public void BuildAProject ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + AssertExists (Path.Combine (intermediateDirectory, "test.dll"), nonEmpty: true); + AssertExists (Path.Combine (intermediateDirectory, "MainPage.xaml.g.cs"), nonEmpty: true); + AssertExists (Path.Combine (intermediateDirectory, "XamlC.stamp")); + } + + /// + /// Tests that XamlG and XamlC targets skip, as well as checking IncrementalClean doesn't delete generated files + /// + [Test] + public void TargetsShouldSkip ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + var mainPageXamlG = Path.Combine (intermediateDirectory, "MainPage.xaml.g.cs"); + var xamlCStamp = Path.Combine (intermediateDirectory, "XamlC.stamp"); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var expectedXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var expectedXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + + //Build again + Build (projectFile); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var actualXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var actualXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + Assert.AreEqual (expectedXamlG, actualXamlG, $"Timestamps should match for {mainPageXamlG}."); + Assert.AreEqual (expectedXamlC, actualXamlC, $"Timestamps should match for {xamlCStamp}."); + } + + /// + /// Checks that XamlG and XamlC files are cleaned + /// + [Test] + public void Clean ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + var mainPageXamlG = Path.Combine (intermediateDirectory, "MainPage.xaml.g.cs"); + var xamlCStamp = Path.Combine (intermediateDirectory, "XamlC.stamp"); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + //Clean + Build (projectFile, "Clean"); + AssertDoesNotExist (mainPageXamlG); + AssertDoesNotExist (xamlCStamp); + } + + [Test] + public void LinkedFile ([Values (false, true)] bool sdkStyle) + { + var folder = Path.Combine (tempDirectory, "A", "B"); + Directory.CreateDirectory (folder); + File.WriteAllText (Path.Combine (folder, "MainPage.xaml"), Xaml.MainPage); + + var project = NewProject (sdkStyle); + var itemGroup = NewElement ("ItemGroup"); + var embeddedResource = NewElement ("EmbeddedResource").WithAttribute ("Include", @"A\B\MainPage.xaml"); + embeddedResource.Add (NewElement ("Link").WithValue (@"Pages\MainPage.xaml")); + itemGroup.Add (embeddedResource); + project.Add (itemGroup); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + AssertExists (Path.Combine (intermediateDirectory, "test.dll"), nonEmpty: true); + AssertExists (Path.Combine (intermediateDirectory, "Pages", "MainPage.xaml.g.cs"), nonEmpty: true); + AssertExists (Path.Combine (intermediateDirectory, "XamlC.stamp")); + } + + //https://github.com/dotnet/project-system/blob/master/docs/design-time-builds.md + //https://daveaglick.com/posts/running-a-design-time-build-with-msbuild-apis + [Test] + public void DesignTimeBuild ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile (@"Pages\MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + + //NOTE: CompileDesignTime target only exists on Windows + var target = Environment.OSVersion.Platform == PlatformID.Win32NT ? "CompileDesignTime" : "Compile"; + Build (projectFile, target, "/p:DesignTimeBuild=True /p:BuildingInsideVisualStudio=True /p:SkipCompilerExecution=True /p:ProvideCommandLineArgs=True"); + + var assembly = Path.Combine (intermediateDirectory, "test.dll"); + var mainPageXamlG = Path.Combine (intermediateDirectory, "Pages", "MainPage.xaml.g.cs"); + var xamlCStamp = Path.Combine (intermediateDirectory, "XamlC.stamp"); + + //The assembly should not be compiled + AssertDoesNotExist (assembly); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var expectedXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var expectedXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + + //Build again, a full build + Build (projectFile); + AssertExists (assembly, nonEmpty: true); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var actualXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var actualXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + Assert.AreEqual (expectedXamlG, actualXamlG, $"Timestamps should match for {mainPageXamlG}."); + Assert.AreNotEqual (expectedXamlC, actualXamlC, $"Timestamps should *not* match for {xamlCStamp}."); + } + + //I believe the designer might invoke this target manually + [Test] + public void UpdateDesignTimeXaml ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile (@"Pages\MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile, "UpdateDesignTimeXaml"); + + AssertExists (Path.Combine (intermediateDirectory, "Pages", "MainPage.xaml.g.cs"), nonEmpty: true); + AssertDoesNotExist (Path.Combine (intermediateDirectory, "XamlC.stamp")); + } + + [Test] + public void AddNewFile ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + var mainPageXamlG = Path.Combine (intermediateDirectory, "MainPage.xaml.g.cs"); + var customViewXamlG = Path.Combine (intermediateDirectory, "CustomView.xaml.g.cs"); + var xamlCStamp = Path.Combine (intermediateDirectory, "XamlC.stamp"); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var expectedXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var expectedXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + + //Build again, after adding a file, this triggers a full XamlG and XamlC + project.Add (AddFile ("CustomView.xaml", "EmbeddedResource", Xaml.CustomView)); + project.Save (projectFile); + Build (projectFile); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (customViewXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var actualXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var actualXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + var actualNewFile = new FileInfo (customViewXamlG).LastAccessTimeUtc; + Assert.AreNotEqual (expectedXamlG, actualXamlG, $"Timestamps should *not* match for {mainPageXamlG}."); + Assert.AreNotEqual (expectedXamlG, actualNewFile, $"Timestamps should *not* match for {actualNewFile}."); + Assert.AreNotEqual (expectedXamlC, actualXamlC, $"Timestamps should *not* match for {xamlCStamp}."); + } + + [Test] + public void TouchXamlFile ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", Xaml.MainPage)); + project.Add (AddFile ("CustomView.xaml", "EmbeddedResource", Xaml.CustomView)); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + var mainPageXamlG = Path.Combine (intermediateDirectory, "MainPage.xaml.g.cs"); + var customViewXamlG = Path.Combine (intermediateDirectory, "CustomView.xaml.g.cs"); + var xamlCStamp = Path.Combine (intermediateDirectory, "XamlC.stamp"); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (customViewXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var expectedMainPageXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var expectedCustomViewXamlG = new FileInfo (customViewXamlG).LastWriteTimeUtc; + var expectedXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + + //Build again, after modifying the timestamp on a Xaml file, should trigger a partial XamlG and full XamlC + //https://github.com/xamarin/xamarin-android/blob/61851599fb1999964bd200ec1c373b6e395933f3/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs#L342 + File.SetLastWriteTimeUtc (customViewXamlG, expectedCustomViewXamlG.AddDays (1)); + File.SetLastAccessTimeUtc (customViewXamlG, expectedCustomViewXamlG.AddDays (1)); + Build (projectFile); + AssertExists (mainPageXamlG, nonEmpty: true); + AssertExists (customViewXamlG, nonEmpty: true); + AssertExists (xamlCStamp); + + var actualMainPageXamlG = new FileInfo (mainPageXamlG).LastWriteTimeUtc; + var actualCustomViewXamlG = new FileInfo (customViewXamlG).LastAccessTimeUtc; + var actualXamlC = new FileInfo (xamlCStamp).LastWriteTimeUtc; + Assert.AreEqual (expectedMainPageXamlG, actualMainPageXamlG, $"Timestamps should match for {mainPageXamlG}."); + Assert.AreNotEqual (expectedMainPageXamlG, actualCustomViewXamlG, $"Timestamps should *not* match for {actualCustomViewXamlG}."); + Assert.AreNotEqual (expectedXamlC, actualXamlC, $"Timestamps should *not* match for {xamlCStamp}."); + } + + [Test] + public void RandomXml ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", "")); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + AssertExists (Path.Combine (intermediateDirectory, "test.dll"), nonEmpty: true); + AssertExists (Path.Combine (intermediateDirectory, "MainPage.xaml.g.cs")); + AssertExists (Path.Combine (intermediateDirectory, "XamlC.stamp")); + } + + [Test, ExpectedException (typeof(AssertionException))] + public void InvalidXml ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.xaml", "EmbeddedResource", "notxmlatall")); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + } + + [Test] + public void RandomEmbeddedResource ([Values (false, true)] bool sdkStyle) + { + var project = NewProject (sdkStyle); + project.Add (AddFile ("MainPage.txt", "EmbeddedResource", "notxmlatall")); + var projectFile = Path.Combine (tempDirectory, "test.csproj"); + project.Save (projectFile); + RestoreIfNeeded (projectFile, sdkStyle); + Build (projectFile); + + AssertExists (Path.Combine (intermediateDirectory, "test.dll"), nonEmpty: true); + AssertDoesNotExist (Path.Combine (intermediateDirectory, "MainPage.txt.g.cs")); + AssertExists (Path.Combine (intermediateDirectory, "XamlC.stamp")); + } + } +} diff --git a/Xamarin.Forms.Xaml.UnitTests/MSBuild/MSBuildXmlExtensions.cs b/Xamarin.Forms.Xaml.UnitTests/MSBuild/MSBuildXmlExtensions.cs new file mode 100644 index 00000000000..747bc803ef8 --- /dev/null +++ b/Xamarin.Forms.Xaml.UnitTests/MSBuild/MSBuildXmlExtensions.cs @@ -0,0 +1,23 @@ +using System.Xml.Linq; + +namespace Xamarin.Forms.Xaml.UnitTests +{ + static class MSBuildXmlExtensions + { + static readonly XNamespace ns = XNamespace.Get ("http://schemas.microsoft.com/developer/msbuild/2003"); + + public static XElement NewElement (string name) => new XElement (ns + name); + + public static XElement WithAttribute (this XElement element, string name, object value) + { + element.SetAttributeValue (name, value); + return element; + } + + public static XElement WithValue (this XElement element, object value) + { + element.SetValue (value); + return element; + } + } +} diff --git a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj index d3b228066c3..9660e8ca4b2 100644 --- a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj +++ b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj @@ -36,12 +36,16 @@ 0672;0219;0414 + + ..\packages\Microsoft.Build.Locator.1.0.13\lib\net46\Microsoft.Build.Locator.dll + ..\packages\NUnit.2.6.4\lib\nunit.framework.dll True + ..\packages\Mono.Cecil.0.10.0-beta7\lib\net40\Mono.Cecil.dll @@ -109,6 +113,8 @@ Bz41296.xaml + + PlatformSpecifics.xaml @@ -135,7 +141,7 @@ - + StyleTests.xaml @@ -611,10 +617,8 @@ - - {57B8B73D-C3B5-4C42-869E-7B2F17D354AC} @@ -1149,15 +1153,9 @@ - - - - - - MSBuild:UpdateDesignTimeXaml diff --git a/Xamarin.Forms.Xaml.UnitTests/XamlgFileLockTests.cs b/Xamarin.Forms.Xaml.UnitTests/XamlgFileLockTests.cs index 1439eaf5b72..970f01075a7 100644 --- a/Xamarin.Forms.Xaml.UnitTests/XamlgFileLockTests.cs +++ b/Xamarin.Forms.Xaml.UnitTests/XamlgFileLockTests.cs @@ -34,13 +34,13 @@ public void XamlFileShouldNotBeLockedAfterFileIsGenerated () BuildEngine= new DummyBuildEngine(), AssemblyName = "test", Language = "C#", - XamlFiles = new[] { item}, - OutputPath = Path.GetDirectoryName(xamlInputFile), + XamlFiles = new[] { item }, + OutputFiles = new[] { new TaskItem(xamlInputFile + ".g.cs") } }; generator.Execute(); - string xamlOutputFile = generator.GeneratedCodeFiles.First().ItemSpec; + string xamlOutputFile = generator.OutputFiles.First().ItemSpec; File.Delete (xamlOutputFile); Assert.DoesNotThrow (() => File.Delete (xamlInputFile)); diff --git a/Xamarin.Forms.Xaml.UnitTests/XamlgTests.cs b/Xamarin.Forms.Xaml.UnitTests/XamlgTests.cs index 92ecdb97b89..3f8d430ee50 100644 --- a/Xamarin.Forms.Xaml.UnitTests/XamlgTests.cs +++ b/Xamarin.Forms.Xaml.UnitTests/XamlgTests.cs @@ -1,10 +1,9 @@ -using System; +using Microsoft.Build.Framework; using NUnit.Framework; -using System.IO; using System.CodeDom; -using Xamarin.Forms.Build.Tasks; -using System.Collections.Generic; +using System.IO; using System.Linq; +using Xamarin.Forms.Build.Tasks; using Xamarin.Forms.Core.UnitTests; @@ -308,5 +307,23 @@ public void FieldModifier() Assert.That(generator.NamedFields.First(cmf => cmf.Name == "publicLabel").Attributes, Is.EqualTo(MemberAttributes.Public)); } } + + [Test] + public void XamlGDifferentInputOutputLengths () + { + var engine = new DummyBuildEngine (); + var generator = new XamlGTask () { + BuildEngine = engine, + AssemblyName = "test", + Language = "C#", + XamlFiles = new ITaskItem [1], + OutputFiles = new ITaskItem [2], + }; + + Assert.IsFalse (generator.Execute (), "XamlGTask.Execute() should fail."); + Assert.AreEqual (1, engine.Errors.Count, "XamlGTask should have 1 error."); + var error = engine.Errors.First (); + Assert.AreEqual ("\"XamlFiles\" refers to 1 item(s), and \"OutputFiles\" refers to 2 item(s). They must have the same number of items.", error.Message); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Xaml.UnitTests/app.config b/Xamarin.Forms.Xaml.UnitTests/app.config index 1953597f659..ebae5454b02 100644 --- a/Xamarin.Forms.Xaml.UnitTests/app.config +++ b/Xamarin.Forms.Xaml.UnitTests/app.config @@ -1,11 +1,15 @@ - + - - + + + + + + - + diff --git a/Xamarin.Forms.Xaml.UnitTests/packages.config b/Xamarin.Forms.Xaml.UnitTests/packages.config index 3beec99cdb0..dcf295094b7 100644 --- a/Xamarin.Forms.Xaml.UnitTests/packages.config +++ b/Xamarin.Forms.Xaml.UnitTests/packages.config @@ -1,6 +1,7 @@  + diff --git a/Xamarin.Forms.Xaml.Xamlg/Xamlg.cs b/Xamarin.Forms.Xaml.Xamlg/Xamlg.cs index 312f525707a..2bccaec238b 100644 --- a/Xamarin.Forms.Xaml.Xamlg/Xamlg.cs +++ b/Xamarin.Forms.Xaml.Xamlg/Xamlg.cs @@ -80,7 +80,7 @@ public static void Main(string[] args) AssemblyName = "test", Language = "C#", XamlFiles = new[] { item }, - OutputPath = Path.GetDirectoryName(f), + OutputFiles = new[] { new TaskItem(f + ".g.cs") } };