Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Update="Microsoft.Build" Version="16.3.0" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.3.1" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Update="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="1.16.30" />
<PackageReference Update="MSTest.TestAdapter" Version="1.1.18" />
<PackageReference Update="MSTest.TestFramework" Version="1.1.18" />
<PackageReference Update="NuGet.Packaging" Version="5.3.1" />
<PackageReference Update="NuGet.ProjectModel" Version="5.3.1" />
<PackageReference Update="Shouldly" Version="3.0.2" />
<PackageReference Update="xunit" Version="2.4.1" />
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.1" />
Expand Down
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![NuGet](https://img.shields.io/nuget/v/MSBuild.ProjectCreation.svg)](https://www.nuget.org/packages/MSBuild.ProjectCreation)
[![NuGet](https://img.shields.io/nuget/dt/MSBuild.ProjectCreation.svg)](https://www.nuget.org/packages/MSBuild.ProjectCreation)

This class library is a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) for generating MSBuild projects. Its primarily for unit tests that need MSBuild projects to do their testing.
This class library is a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) for generating MSBuild projects and NuGet package repositories. Its primarily for unit tests that need MSBuild projects to do their testing.

## Example
You want to test a custom MSBuild task that you are building so your unit tests need to generate a project that you can build with MSBuild. The following code would generate the necessary project:
Expand Down Expand Up @@ -125,7 +125,7 @@ Your extension methods should extend the `ProjectCreatorTemplates` class so they
```C#
public static class ExtensionMethods
{
public ProjectCreator LogsMessage(this ProjectCreatorTemplates template, string text, string path = null, MessageImportance ? importance = null, string condition = null)
public ProjectCreator LogsMessage(this ProjectCreatorTemplates template, string text, string path = null, MessageImportance ? importance = null, string condition = null)
{
return ProjectCreator.Create(path)
.TaskMessage(text, importance, condition);
Expand Down Expand Up @@ -158,3 +158,32 @@ And the resulting project would look like this:
</Target>
</Project>
```

# Package Repositories
NuGet and MSBuild are very tightly coupled and a lot of times you need packages available when building projects.

## Example

Create a package repository with a package that supports two target frameworks:

```C#
PackageRepository.Create(rootPath)
.Package("MyPackage", "1.2.3", out PackageIdentify package)
.Library("net472")
.Library("netstandard2.0");
```

The resulting package would have a `lib\net472\MyPackage.dll` and `lib\netstandard2.0\MyPackage.dll` class library. This allows you to restore and build projects that consume the packages

```C#
PackageRepository.Create(rootPath)
.Package("MyPackage", "1.0.0", out PackageIdentify package)
.Library("netstandard2.0");

ProjectCreator.Templates.SdkCsproj()
.ItemPackageReference(package)
.Save(Path.Combine(rootPath, "ClassLibraryA", "ClassLibraryA.csproj"))
.TryBuild(restore: true, out bool result, out BuildOutput buildOutput);
```

The result would be a project that references the `MyPackage` package and would restore and build accordingly.
54 changes: 39 additions & 15 deletions src/MSBuildProjectCreator.UnitTests/BuildTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the MIT license.

using Microsoft.Build.Execution;
using NuGet.Packaging.Core;
using Shouldly;
using System.Collections.Generic;
using System.IO;
Expand All @@ -13,6 +14,29 @@ namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests
{
public class BuildTests : TestBase
{
#if NETCOREAPP
[Fact(Skip = "Does not work yet on .NET Core")]
#else
[Fact]
#endif
public void BuildCanConsumePackage()
{
PackageRepository packageRepository = PackageRepository.Create(TestRootPath)
.Package("PackageB", "1.0", out PackageIdentity packageB)
.Library("net45")
.Package("PackageA", "1.0.0", out PackageIdentity packageA)
.Dependency(packageB, "net45")
.Library("net45");

ProjectCreator.Templates.SdkCsproj(
targetFramework: "net45")
.ItemPackageReference(packageA)
.Save(Path.Combine(TestRootPath, "ClassLibraryA", "ClassLibraryA.csproj"))
.TryBuild(restore: true, out bool result, out BuildOutput buildOutput);

result.ShouldBeTrue(buildOutput.GetConsoleLog());
}

[Fact]
public void BuildTargetOutputsTest()
{
Expand All @@ -33,21 +57,6 @@ public void BuildTargetOutputsTest()
item.Value.Items.Select(i => i.ItemSpec).ShouldBe(new[] { "E32099C7AF4E481885B624E5600C718A", "7F38E64414104C6182F492B535926187" });
}

[Fact]
public void RestoreTargetCanBeRun()
{
ProjectCreator
.Create(Path.Combine(TestRootPath, "project1.proj"))
.Target("Restore")
.TaskMessage("312D2E6ABDDC4735B437A016CED1A68E", Framework.MessageImportance.High, condition: "'$(MSBuildRestoreSessionId)' != ''")
.TaskError("MSBuildRestoreSessionId was not defined", condition: "'$(MSBuildRestoreSessionId)' == ''")
.TryRestore(out bool result, out BuildOutput buildOutput);

result.ShouldBeTrue(buildOutput.GetConsoleLog());

buildOutput.MessageEvents.High.ShouldContain(i => i.Message == "312D2E6ABDDC4735B437A016CED1A68E" && i.Importance == Framework.MessageImportance.High, buildOutput.GetConsoleLog());
}

[Fact]
public void CanRestoreAndBuild()
{
Expand All @@ -66,5 +75,20 @@ public void CanRestoreAndBuild()

buildOutput.MessageEvents.High.ShouldContain(i => i.Message == "Building...", buildOutput.GetConsoleLog());
}

[Fact]
public void RestoreTargetCanBeRun()
{
ProjectCreator
.Create(Path.Combine(TestRootPath, "project1.proj"))
.Target("Restore")
.TaskMessage("312D2E6ABDDC4735B437A016CED1A68E", Framework.MessageImportance.High, condition: "'$(MSBuildRestoreSessionId)' != ''")
.TaskError("MSBuildRestoreSessionId was not defined", condition: "'$(MSBuildRestoreSessionId)' == ''")
.TryRestore(out bool result, out BuildOutput buildOutput);

result.ShouldBeTrue(buildOutput.GetConsoleLog());

buildOutput.MessageEvents.High.ShouldContain(i => i.Message == "312D2E6ABDDC4735B437A016CED1A68E" && i.Importance == Framework.MessageImportance.High, buildOutput.GetConsoleLog());
}
}
}
32 changes: 32 additions & 0 deletions src/MSBuildProjectCreator.UnitTests/ExtensionMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Jeff Kluge. All rights reserved.
//
// Licensed under the MIT license.

using Shouldly;
using System.IO;

namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests
{
internal static class ExtensionMethods
{
public static FileInfo ShouldExist(this FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
throw new ShouldAssertException($"The file \"{fileInfo.FullName}\" should exist but does not");
}

return fileInfo;
}

public static DirectoryInfo ShouldExist(this DirectoryInfo directoryInfo)
{
if (!directoryInfo.Exists)
{
throw new ShouldAssertException($"The directory \"{directoryInfo.FullName}\" should exist but does not");
}

return directoryInfo;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) Jeff Kluge. All rights reserved.
//
// Licensed under the MIT license.

using Microsoft.Build.Utilities.ProjectCreation.Resources;
using Shouldly;
using System;
using System.IO;
using Xunit;

namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests.PackageRepositoryTests
{
public class BuildLogicTests : TestBase
{
[Fact]
public void BuildLogicRequiresPackage()
{
InvalidOperationException exception;

exception = Should.Throw<InvalidOperationException>(() =>
{
PackageRepository.Create(TestRootPath)
.BuildMultiTargetingProps();
});

exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage);

exception = Should.Throw<InvalidOperationException>(() =>
{
PackageRepository.Create(TestRootPath)
.BuildMultiTargetingTargets();
});

exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage);

exception = Should.Throw<InvalidOperationException>(() =>
{
PackageRepository.Create(TestRootPath)
.BuildTransitiveProps();
});

exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage);

exception = Should.Throw<InvalidOperationException>(() =>
{
PackageRepository.Create(TestRootPath)
.BuildTransitiveTargets();
});

exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage);

exception = Should.Throw<InvalidOperationException>(() =>
{
PackageRepository.Create(TestRootPath)
.BuildProps();
});

exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage);

exception = Should.Throw<InvalidOperationException>(() =>
{
PackageRepository.Create(TestRootPath)
.BuildTargets();
});

exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage);
}

[Fact]
public void BuildMultiTargetingTest()
{
PackageRepository.Create(TestRootPath)
.Package("PackageA", "2.0")
.BuildMultiTargetingProps(out ProjectCreator buildMultiTargetingPropsProject)
.BuildMultiTargetingTargets(out ProjectCreator buildMultiTargetingTargetsProject);

buildMultiTargetingPropsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildMultiTargeting\PackageA.props");
buildMultiTargetingTargetsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildMultiTargeting\PackageA.targets");

File.Exists(buildMultiTargetingPropsProject.FullPath).ShouldBeTrue();
File.Exists(buildMultiTargetingTargetsProject.FullPath).ShouldBeTrue();
}

[Fact]
public void BuildPropsTest()
{
PackageRepository.Create(TestRootPath)
.Package("PackageA", "2.0")
.BuildProps(out ProjectCreator buildPropsProject)
.BuildTargets(out ProjectCreator buildTargetsProject);

buildPropsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\build\PackageA.props");
buildTargetsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\build\PackageA.targets");

File.Exists(buildPropsProject.FullPath).ShouldBeTrue();
File.Exists(buildTargetsProject.FullPath).ShouldBeTrue();
}

[Fact]
public void BuildTransitiveTest()
{
PackageRepository.Create(TestRootPath)
.Package("PackageA", "2.0")
.BuildTransitiveProps(out ProjectCreator buildTransitivePropsProject)
.BuildTransitiveTargets(out ProjectCreator buildTransitiveTargetsProject);

buildTransitivePropsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildTransitive\PackageA.props");
buildTransitiveTargetsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildTransitive\PackageA.targets");

File.Exists(buildTransitivePropsProject.FullPath).ShouldBeTrue();
File.Exists(buildTransitiveTargetsProject.FullPath).ShouldBeTrue();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Jeff Kluge. All rights reserved.
//
// Licensed under the MIT license.

using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Versioning;
using Shouldly;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Xunit;

namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests.PackageRepositoryTests
{
public class DependencyTests : TestBase
{
[Fact]
public void CanAddDependenciesToMultipleGroups()
{
PackageRepository.Create(TestRootPath)
.Package("PackageA", "1.0.0", out PackageIdentity package)
.Dependency("PackageB", "1.0.0", "net45")
.Dependency("PackageB", "1.0.0", "net46")
.Dependency("PackageB", "1.0.0", "netstandard2.0");

ValidatePackageDependencies(
package,
new List<PackageDependencyGroup>
{
new PackageDependencyGroup(
FrameworkConstants.CommonFrameworks.Net45,
new List<PackageDependency>
{
new PackageDependency("PackageB", VersionRange.Parse("1.0.0")),
}),
new PackageDependencyGroup(
FrameworkConstants.CommonFrameworks.Net46,
new List<PackageDependency>
{
new PackageDependency("PackageB", VersionRange.Parse("1.0.0")),
}),
new PackageDependencyGroup(
FrameworkConstants.CommonFrameworks.NetStandard20,
new List<PackageDependency>
{
new PackageDependency("PackageB", VersionRange.Parse("1.0.0")),
}),
});
}

[Fact]
public void CanAddMultipleDependenciesToSameGroup()
{
PackageRepository.Create(TestRootPath)
.Package("PackageA", "1.0.0", out PackageIdentity package)
.Dependency("PackageB", "1.0.0", "net45")
.Dependency("PackageC", "1.1.0", "net45")
.Dependency("PackageD", "1.2.0", "net45");

ValidatePackageDependencies(
package,
new List<PackageDependencyGroup>
{
new PackageDependencyGroup(
FrameworkConstants.CommonFrameworks.Net45,
new List<PackageDependency>
{
new PackageDependency("PackageB", VersionRange.Parse("1.0.0")),
new PackageDependency("PackageC", VersionRange.Parse("1.1.0")),
new PackageDependency("PackageD", VersionRange.Parse("1.2.0")),
}),
});
}

private void ValidatePackageDependencies(PackageIdentity package, IEnumerable<PackageDependencyGroup> expectedDependencyGroups)
{
FileInfo nuspecFile = new FileInfo(VersionFolderPathResolver.GetManifestFilePath(package.Id, package.Version));

nuspecFile.ShouldExist();

using (FileStream stream = File.OpenRead(nuspecFile.FullName))
{
Manifest manifest = Manifest.ReadFrom(stream, validateSchema: false);

List<PackageDependencyGroup> dependencyGroups = manifest.Metadata.DependencyGroups.ToList();

dependencyGroups.ShouldBe(expectedDependencyGroups);
}
}
}
}
Loading