Update for SDK 2.2.100-preview1 (#15)

This commit is contained in:
Mike Harder 2018-08-14 18:46:51 -07:00 committed by GitHub
parent f385308503
commit 51e6492b44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 421 additions and 166 deletions

View File

@ -10,7 +10,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NuGet.Versioning" Version="4.7.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
</ItemGroup>

View File

@ -15,18 +15,21 @@ namespace AspNetCoreSdkTests
public static RuntimeIdentifier Linux_x64 = new RuntimeIdentifier() {
Name = "linux-x64",
OSPlatforms = new[] { OSPlatform.Linux, },
ExecutableFileExtension = string.Empty,
};
public static RuntimeIdentifier OSX_x64 = new RuntimeIdentifier()
{
Name = "osx-x64",
OSPlatforms = new[] { OSPlatform.OSX, },
ExecutableFileExtension = string.Empty,
};
public static RuntimeIdentifier Win_x64 = new RuntimeIdentifier()
{
Name = "win-x64",
OSPlatforms = new[] { OSPlatform.Windows, },
ExecutableFileExtension = ".exe",
};
public static IEnumerable<RuntimeIdentifier> All = new[]
@ -43,6 +46,7 @@ namespace AspNetCoreSdkTests
public string RuntimeArgument => (this == None) ? string.Empty : $"--runtime {Name}";
public string Path => (this == None) ? string.Empty : Name;
public IEnumerable<OSPlatform> OSPlatforms { get; private set; }
public string ExecutableFileExtension { get; private set; }
public override string ToString() => Name;
}

View File

@ -1,5 +1,6 @@
using AspNetCoreSdkTests.Templates;
using AspNetCoreSdkTests.Util;
using NuGet.Versioning;
using NUnit.Framework;
using System;
using System.Collections.Generic;
@ -19,6 +20,13 @@ namespace AspNetCoreSdkTests
CollectionAssert.AreEquivalent(template.ExpectedObjFilesAfterRestore, template.ObjFilesAfterRestore);
}
[Test]
[TestCaseSource(nameof(RestoreData))]
public void RestoreIncremental(Template template)
{
CollectionAssert.AreEquivalent(template.ExpectedObjFilesAfterRestore, template.ObjFilesAfterRestoreIncremental);
}
[Test]
[TestCaseSource(nameof(BuildData))]
public void Build(Template template)
@ -27,11 +35,30 @@ namespace AspNetCoreSdkTests
CollectionAssert.AreEquivalent(template.ExpectedBinFilesAfterBuild, template.BinFilesAfterBuild);
}
[Test]
[TestCaseSource(nameof(BuildData))]
public void BuildIncremental(Template template)
{
CollectionAssert.AreEquivalent(template.ExpectedObjFilesAfterBuild, template.ObjFilesAfterBuildIncremental);
CollectionAssert.AreEquivalent(template.ExpectedBinFilesAfterBuild, template.BinFilesAfterBuildIncremental);
}
[Test]
[TestCaseSource(nameof(PublishData))]
public void Publish(Template template)
{
CollectionAssert.AreEquivalent(template.ExpectedFilesAfterPublish, template.FilesAfterPublish);
var expected = template.ExpectedFilesAfterPublish;
var actual = template.FilesAfterPublish;
CollectionAssert.AreEquivalent(expected, actual);
}
[Test]
[TestCaseSource(nameof(PublishData))]
public void PublishIncremental(Template template)
{
var expected = template.ExpectedFilesAfterPublish;
var actual = template.FilesAfterPublishIncremental;
CollectionAssert.AreEquivalent(expected, actual);
}
[Test]
@ -85,11 +112,11 @@ namespace AspNetCoreSdkTests
private static IEnumerable<Template> GetTemplates(RuntimeIdentifier runtimeIdentifier)
{
// Offline restore is broken in SDK 2.1.301 (https://github.com/aspnet/Universe/issues/1220)
var offlinePackageSource = (DotNetUtil.SdkVersion == new Version(2, 1, 301)) ?
var offlinePackageSource = (DotNetUtil.SdkVersion == new SemanticVersion(2, 1, 301)) ?
NuGetPackageSource.NuGetOrg : NuGetPackageSource.None;
// Pre-release SDKs require a private nuget feed
var onlinePackageSource = (DotNetUtil.SdkVersion == new Version(2, 1, 401)) ?
var onlinePackageSource = (DotNetUtil.SdkVersion.IsPrerelease) ?
NuGetPackageSource.EnvironmentVariableAndNuGetOrg : NuGetPackageSource.NuGetOrg;
if (runtimeIdentifier == RuntimeIdentifier.None)

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@ -11,26 +13,52 @@ namespace AspNetCoreSdkTests.Templates
public override string Name => "angular";
// Remove generated hashes since they may vary by platform
public override IEnumerable<string> FilesAfterPublish =>
base.FilesAfterPublish.Select(f => Regex.Replace(f, @"\.[0-9a-f]{20}\.", ".[HASH]."));
protected override IEnumerable<string> NormalizeFilesAfterPublish(IEnumerable<string> filesAfterPublish)
{
// Remove generated hashes since they may vary by platform
return base.NormalizeFilesAfterPublish(filesAfterPublish)
.Select(f => Regex.Replace(f, @"\.[0-9a-f]{20}\.", ".[HASH]."));
}
public override IEnumerable<string> ExpectedFilesAfterPublish =>
base.ExpectedFilesAfterPublish
.Concat(new[]
private IDictionary<string, Func<IEnumerable<string>>> _additionalFilesAfterPublish =>
new Dictionary<string, Func<IEnumerable<string>>>()
{
Path.Combine("wwwroot", "favicon.ico"),
Path.Combine("ClientApp", "dist", "3rdpartylicenses.txt"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].woff2"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].svg"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].ttf"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].eot"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].woff"),
Path.Combine("ClientApp", "dist", "index.html"),
Path.Combine("ClientApp", "dist", "inline.[HASH].bundle.js"),
Path.Combine("ClientApp", "dist", "main.[HASH].bundle.js"),
Path.Combine("ClientApp", "dist", "polyfills.[HASH].bundle.js"),
Path.Combine("ClientApp", "dist", "styles.[HASH].bundle.css"),
});
{ "common", () => new[]
{
Path.Combine("wwwroot", "favicon.ico"),
Path.Combine("ClientApp", "dist", "3rdpartylicenses.txt"),
Path.Combine("ClientApp", "dist", "index.html"),
}
},
{ "netcoreapp2.1", () =>
_additionalFilesAfterPublish["common"]()
.Concat(new[]
{
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].woff2"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].svg"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].ttf"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].eot"),
Path.Combine("ClientApp", "dist", "glyphicons-halflings-regular.[HASH].woff"),
Path.Combine("ClientApp", "dist", $"inline.[HASH].bundle.js"),
Path.Combine("ClientApp", "dist", $"main.[HASH].bundle.js"),
Path.Combine("ClientApp", "dist", $"polyfills.[HASH].bundle.js"),
Path.Combine("ClientApp", "dist", $"styles.[HASH].bundle.css"),
})
},
{ "netcoreapp2.2", () =>
_additionalFilesAfterPublish["common"]()
.Concat(new[]
{
Path.Combine("ClientApp", "dist", $"runtime.[HASH].js"),
Path.Combine("ClientApp", "dist", $"main.[HASH].js"),
Path.Combine("ClientApp", "dist", $"polyfills.[HASH].js"),
Path.Combine("ClientApp", "dist", $"styles.[HASH].css"),
})
},
};
public override IEnumerable<string> ExpectedFilesAfterPublish =>
base.ExpectedFilesAfterPublish
.Concat(_additionalFilesAfterPublish[DotNetUtil.TargetFrameworkMoniker]());
}
}

View File

@ -1,4 +1,5 @@
using System;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -12,7 +13,7 @@ namespace AspNetCoreSdkTests.Templates
public override string Name => "console";
public override string OutputPath => Path.Combine("Debug", "netcoreapp2.1", RuntimeIdentifier.Path);
public override string OutputPath => Path.Combine("Debug", DotNetUtil.TargetFrameworkMoniker, RuntimeIdentifier.Path);
public override TemplateType Type => TemplateType.ConsoleApplication;
@ -22,12 +23,12 @@ namespace AspNetCoreSdkTests.Templates
{ RuntimeIdentifier.None, () => Enumerable.Empty<string>() },
{ RuntimeIdentifier.Win_x64, () => new[]
{
Path.Combine("netcoreapp2.1", RuntimeIdentifier.Path, "host", $"{Name}.exe"),
Path.Combine(DotNetUtil.TargetFrameworkMoniker, RuntimeIdentifier.Path, "host", $"{Name}.exe"),
}
},
{ RuntimeIdentifier.Linux_x64, () => new[]
{
Path.Combine("netcoreapp2.1", RuntimeIdentifier.Path, "host", $"{Name}"),
Path.Combine(DotNetUtil.TargetFrameworkMoniker, RuntimeIdentifier.Path, "host", $"{Name}"),
}
},
{ RuntimeIdentifier.OSX_x64, () => _additionalObjFilesAfterBuild[RuntimeIdentifier.Linux_x64]() },
@ -74,11 +75,13 @@ namespace AspNetCoreSdkTests.Templates
base.ExpectedBinFilesAfterBuild
.Concat(_additionalBinFilesAfterBuild[RuntimeIdentifier]());
// A few files included in self-contained deployments contain version numbers in the filename, which must
// be replaced so tests can pass on all versions.
public override IEnumerable<string> FilesAfterPublish =>
base.FilesAfterPublish
.Select(f => Regex.Replace(f, @"_amd64_amd64_[0-9\.]+\.dll$", "_amd64_amd64_[VERSION].dll"));
protected override IEnumerable<string> NormalizeFilesAfterPublish(IEnumerable<string> filesAfterPublish)
{
// A few files included in self-contained deployments contain version numbers in the filename, which must
// be replaced so tests can pass on all versions.
return base.NormalizeFilesAfterPublish(filesAfterPublish)
.Select(f => Regex.Replace(f, @"_amd64_amd64_[0-9\.]+\.dll$", "_amd64_amd64_[VERSION].dll"));
}
private Func<IEnumerable<string>> _additionalFilesAfterPublishCommon = () => new[]
{

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -12,19 +14,33 @@ namespace AspNetCoreSdkTests.Templates
protected override string RazorPath => "Views";
private IDictionary<string, Func<IEnumerable<string>>> _additionalObjFilesAfterBuild =>
new Dictionary<string, Func<IEnumerable<string>>>()
{
{ "common", () => new[]
{
Path.Combine("Razor", RazorPath, "_ViewStart.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "Index.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "Privacy.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_CookieConsentPartial.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_Layout.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_ValidationScriptsPartial.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "Error.g.cshtml.cs"),
}
},
{ "netcoreapp2.1", () =>
_additionalObjFilesAfterBuild["common"]()
.Concat(new[]
{
Path.Combine("Razor", RazorPath, "Home", "About.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "Contact.g.cshtml.cs"),
})
},
{ "netcoreapp2.2", () => _additionalObjFilesAfterBuild["common"]() },
};
public override IEnumerable<string> ExpectedObjFilesAfterBuild =>
base.ExpectedObjFilesAfterBuild
.Concat(new[]
{
Path.Combine("Razor", RazorPath, "_ViewStart.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "About.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "Contact.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "Index.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Home", "Privacy.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_CookieConsentPartial.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_Layout.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_ValidationScriptsPartial.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "Error.g.cshtml.cs"),
}.Select(p => Path.Combine(OutputPath, p)));
.Concat(_additionalObjFilesAfterBuild[DotNetUtil.TargetFrameworkMoniker]().Select(p => Path.Combine(OutputPath, p)));
}
}

View File

@ -1,4 +1,5 @@
using System;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -21,14 +22,14 @@ namespace AspNetCoreSdkTests.Templates
_additionalObjFilesAfterBuild[RuntimeIdentifier.None]()
.Concat(new[]
{
Path.Combine("netcoreapp2.1", RuntimeIdentifier.Path, "host", $"{Name}.exe"),
Path.Combine(DotNetUtil.TargetFrameworkMoniker, RuntimeIdentifier.Path, "host", $"{Name}.exe"),
})
},
{ RuntimeIdentifier.Linux_x64, () =>
_additionalObjFilesAfterBuild[RuntimeIdentifier.None]()
.Concat(new[]
{
Path.Combine("netcoreapp2.1", RuntimeIdentifier.Path, "host", $"{Name}"),
Path.Combine(DotNetUtil.TargetFrameworkMoniker, RuntimeIdentifier.Path, "host", $"{Name}"),
})
},
{ RuntimeIdentifier.OSX_x64, () => _additionalObjFilesAfterBuild[RuntimeIdentifier.Linux_x64]() },

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -6,51 +8,84 @@ namespace AspNetCoreSdkTests.Templates
{
public abstract class RazorBootstrapJQueryTemplate : RazorApplicationBaseTemplate
{
public override IEnumerable<string> ExpectedFilesAfterPublish =>
base.ExpectedFilesAfterPublish
.Concat(new[]
private IDictionary<string, Func<IEnumerable<string>>> _additionalFilesAfterPublish =>
new Dictionary<string, Func<IEnumerable<string>>>()
{
Path.Combine("wwwroot", "favicon.ico"),
Path.Combine("wwwroot", "css", "site.css"),
Path.Combine("wwwroot", "css", "site.min.css"),
Path.Combine("wwwroot", "images", "banner1.svg"),
Path.Combine("wwwroot", "images", "banner2.svg"),
Path.Combine("wwwroot", "images", "banner3.svg"),
Path.Combine("wwwroot", "js", "site.js"),
Path.Combine("wwwroot", "js", "site.min.js"),
Path.Combine("wwwroot", "lib", "bootstrap", ".bower.json"),
Path.Combine("wwwroot", "lib", "bootstrap", "LICENSE"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.min.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.min.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.eot"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.svg"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.ttf"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.woff"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.woff2"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.min.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "npm.js"),
Path.Combine("wwwroot", "lib", "jquery", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery", "LICENSE.txt"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.js"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.min.js"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.min.map"),
Path.Combine("wwwroot", "lib", "jquery-validation", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery-validation", "LICENSE.md"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "additional-methods.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "additional-methods.min.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "jquery.validate.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "jquery.validate.min.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", "jquery.validate.unobtrusive.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", "jquery.validate.unobtrusive.min.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", "LICENSE.txt"),
});
{ "common", () => new[]
{
Path.Combine("wwwroot", "favicon.ico"),
Path.Combine("wwwroot", "css", "site.css"),
Path.Combine("wwwroot", "css", "site.min.css"),
Path.Combine("wwwroot", "images", "banner1.svg"),
Path.Combine("wwwroot", "images", "banner2.svg"),
Path.Combine("wwwroot", "images", "banner3.svg"),
Path.Combine("wwwroot", "js", "site.js"),
Path.Combine("wwwroot", "js", "site.min.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "LICENSE"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.min.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.min.js"),
Path.Combine("wwwroot", "lib", "jquery", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery", "LICENSE.txt"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.js"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.min.js"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.min.map"),
Path.Combine("wwwroot", "lib", "jquery-validation", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery-validation", "LICENSE.md"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "additional-methods.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "additional-methods.min.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "jquery.validate.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "jquery.validate.min.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", "jquery.validate.unobtrusive.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", "jquery.validate.unobtrusive.min.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", "LICENSE.txt"),
}
},
{ "netcoreapp2.1", () =>
_additionalFilesAfterPublish["common"]()
.Concat(new[]
{
Path.Combine("wwwroot", "lib", "bootstrap", ".bower.json"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.min.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-theme.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.eot"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.svg"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.ttf"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.woff"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "fonts", "glyphicons-halflings-regular.woff2"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "npm.js"),
Path.Combine("wwwroot", "lib", "jquery-validation-unobtrusive", ".bower.json"),
})
},
{ "netcoreapp2.2", () =>
_additionalFilesAfterPublish["common"]()
.Concat(new[]
{
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-grid.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-grid.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-grid.min.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-grid.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-reboot.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-reboot.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-reboot.min.css"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap-reboot.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.bundle.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.bundle.js.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.bundle.min.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.bundle.min.js.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.js.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.min.js.map"),
})
},
};
public override IEnumerable<string> ExpectedFilesAfterPublish =>
base.ExpectedFilesAfterPublish
.Concat(_additionalFilesAfterPublish[DotNetUtil.TargetFrameworkMoniker]());
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -12,19 +14,33 @@ namespace AspNetCoreSdkTests.Templates
protected override string RazorPath => "Pages";
public override IEnumerable<string> ExpectedObjFilesAfterBuild =>
base.ExpectedObjFilesAfterBuild
.Concat(new[]
private IDictionary<string, Func<IEnumerable<string>>> _additionalObjFilesAfterBuild =>
new Dictionary<string, Func<IEnumerable<string>>>()
{
Path.Combine("Razor", RazorPath, "_ViewStart.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "About.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Contact.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Error.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Index.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Privacy.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_CookieConsentPartial.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_Layout.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_ValidationScriptsPartial.g.cshtml.cs"),
}.Select(p => Path.Combine(OutputPath, p)));
{ "common", () => new[]
{
Path.Combine("Razor", RazorPath, "_ViewStart.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Error.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Index.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Privacy.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_CookieConsentPartial.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_Layout.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Shared", "_ValidationScriptsPartial.g.cshtml.cs"),
}
},
{ "netcoreapp2.1", () =>
_additionalObjFilesAfterBuild["common"]()
.Concat(new[]
{
Path.Combine("Razor", RazorPath, "About.g.cshtml.cs"),
Path.Combine("Razor", RazorPath, "Contact.g.cshtml.cs"),
})
},
{ "netcoreapp2.2", () => _additionalObjFilesAfterBuild["common"]() },
};
public override IEnumerable<string> ExpectedObjFilesAfterBuild =>
base.ExpectedObjFilesAfterBuild
.Concat(_additionalObjFilesAfterBuild[DotNetUtil.TargetFrameworkMoniker]().Select(p => Path.Combine(OutputPath, p)));
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@ -11,28 +13,45 @@ namespace AspNetCoreSdkTests.Templates
public override string Name => "react";
// Remove generated hashes since they may var by platform
public override IEnumerable<string> FilesAfterPublish =>
base.FilesAfterPublish.Select(f => Regex.Replace(f, @"\.[0-9a-f]{8}\.", ".[HASH]."));
protected override IEnumerable<string> NormalizeFilesAfterPublish(IEnumerable<string> filesAfterPublish)
{
// Remove generated hashes since they may vary by platform
return base.NormalizeFilesAfterPublish(filesAfterPublish)
.Select(f => Regex.Replace(f, @"\.[0-9a-f]{8}\.", ".[HASH]."));
}
private IDictionary<string, Func<IEnumerable<string>>> _additionalFilesAfterPublish =>
new Dictionary<string, Func<IEnumerable<string>>>()
{
{ "common", () => new[]
{
Path.Combine("ClientApp", "build", "asset-manifest.json"),
Path.Combine("ClientApp", "build", "favicon.ico"),
Path.Combine("ClientApp", "build", "index.html"),
Path.Combine("ClientApp", "build", "manifest.json"),
Path.Combine("ClientApp", "build", "service-worker.js"),
Path.Combine("ClientApp", "build", "static", "css", "main.[HASH].css"),
Path.Combine("ClientApp", "build", "static", "css", "main.[HASH].css.map"),
Path.Combine("ClientApp", "build", "static", "js", "main.[HASH].js"),
Path.Combine("ClientApp", "build", "static", "js", "main.[HASH].js.map"),
}
},
{ "netcoreapp2.1", () =>
_additionalFilesAfterPublish["common"]()
.Concat(new[]
{
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].woff2"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].svg"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].ttf"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].eot"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].woff"),
})
},
{ "netcoreapp2.2", () => _additionalFilesAfterPublish["common"]() },
};
public override IEnumerable<string> ExpectedFilesAfterPublish =>
base.ExpectedFilesAfterPublish
.Concat(new[]
{
Path.Combine("ClientApp", "build", "asset-manifest.json"),
Path.Combine("ClientApp", "build", "favicon.ico"),
Path.Combine("ClientApp", "build", "index.html"),
Path.Combine("ClientApp", "build", "manifest.json"),
Path.Combine("ClientApp", "build", "service-worker.js"),
Path.Combine("ClientApp", "build", "static", "css", "main.[HASH].css"),
Path.Combine("ClientApp", "build", "static", "css", "main.[HASH].css.map"),
Path.Combine("ClientApp", "build", "static", "js", "main.[HASH].js"),
Path.Combine("ClientApp", "build", "static", "js", "main.[HASH].js.map"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].woff2"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].svg"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].ttf"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].eot"),
Path.Combine("ClientApp", "build", "static", "media", "glyphicons-halflings-regular.[HASH].woff"),
});
.Concat(_additionalFilesAfterPublish[DotNetUtil.TargetFrameworkMoniker]());
}
}

View File

@ -30,8 +30,11 @@ namespace AspNetCoreSdkTests.Templates
}
private Lazy<IEnumerable<string>> _objFilesAfterRestore;
private Lazy<IEnumerable<string>> _objFilesAfterRestoreIncremental;
private Lazy<(IEnumerable<string> ObjFiles, IEnumerable<string> BinFiles)> _filesAfterBuild;
private Lazy<(IEnumerable<string> ObjFiles, IEnumerable<string> BinFiles)> _filesAfterBuildIncremental;
private Lazy<IEnumerable<string>> _filesAfterPublish;
private Lazy<IEnumerable<string>> _filesAfterPublishIncremental;
private Lazy<(HttpResponseMessage Http, HttpResponseMessage Https, string ServerOutput, string ServerError )> _httpResponsesAfterRun;
private Lazy<(HttpResponseMessage Http, HttpResponseMessage Https, string ServerOutput, string ServerError)> _httpResponsesAfterExec;
@ -43,12 +46,21 @@ namespace AspNetCoreSdkTests.Templates
_objFilesAfterRestore = new Lazy<IEnumerable<string>>(
GetObjFilesAfterRestore, LazyThreadSafetyMode.ExecutionAndPublication);
_objFilesAfterRestoreIncremental = new Lazy<IEnumerable<string>>(
GetObjFilesAfterRestoreIncremental, LazyThreadSafetyMode.ExecutionAndPublication);
_filesAfterBuild = new Lazy<(IEnumerable<string> ObjFiles, IEnumerable<string> BinFiles)>(
GetFilesAfterBuild, LazyThreadSafetyMode.ExecutionAndPublication);
_filesAfterBuildIncremental = new Lazy<(IEnumerable<string> ObjFiles, IEnumerable<string> BinFiles)>(
GetFilesAfterBuildIncremental, LazyThreadSafetyMode.ExecutionAndPublication);
_filesAfterPublish = new Lazy<IEnumerable<string>>(
GetFilesAfterPublish, LazyThreadSafetyMode.ExecutionAndPublication);
_filesAfterPublishIncremental = new Lazy<IEnumerable<string>>(
GetFilesAfterPublishIncremental, LazyThreadSafetyMode.ExecutionAndPublication);
_httpResponsesAfterRun = new Lazy<(HttpResponseMessage Http, HttpResponseMessage Https, string ServerOutput, string ServerError)>(
GetHttpResponsesAfterRun, LazyThreadSafetyMode.ExecutionAndPublication);
@ -56,7 +68,7 @@ namespace AspNetCoreSdkTests.Templates
GetHttpResponsesAfterExec, LazyThreadSafetyMode.ExecutionAndPublication);
}
public override string ToString() => $"{Name}, source: {NuGetPackageSource}, rid: {RuntimeIdentifier}, sdk: {DotNetUtil.SdkVersion}";
public override string ToString() => $"{Name}, source: {NuGetPackageSource}, rid: {RuntimeIdentifier}, sdk: {DotNetUtil.SdkVersion}, runtime: {DotNetUtil.RuntimeVersion}";
private string TempDir => Path.Combine(AssemblySetUp.TempDir, Name, NuGetPackageSource.Name, RuntimeIdentifier.Name );
@ -66,9 +78,13 @@ namespace AspNetCoreSdkTests.Templates
public virtual string RelativeUrl => string.Empty;
public IEnumerable<string> ObjFilesAfterRestore => _objFilesAfterRestore.Value;
public IEnumerable<string> ObjFilesAfterRestoreIncremental => _objFilesAfterRestoreIncremental.Value;
public IEnumerable<string> ObjFilesAfterBuild => _filesAfterBuild.Value.ObjFiles;
public IEnumerable<string> BinFilesAfterBuild => _filesAfterBuild.Value.BinFiles;
public virtual IEnumerable<string> FilesAfterPublish => _filesAfterPublish.Value;
public IEnumerable<string> ObjFilesAfterBuildIncremental => _filesAfterBuildIncremental.Value.ObjFiles;
public IEnumerable<string> BinFilesAfterBuildIncremental => _filesAfterBuildIncremental.Value.BinFiles;
public IEnumerable<string> FilesAfterPublish => NormalizeFilesAfterPublish(_filesAfterPublish.Value);
public IEnumerable<string> FilesAfterPublishIncremental => NormalizeFilesAfterPublish(_filesAfterPublishIncremental.Value);
public HttpResponseMessage HttpResponseAfterRun => _httpResponsesAfterRun.Value.Http;
public HttpResponseMessage HttpsResponseAfterRun => _httpResponsesAfterRun.Value.Https;
public string ServerOutputAfterRun => _httpResponsesAfterRun.Value.ServerOutput;
@ -92,18 +108,47 @@ namespace AspNetCoreSdkTests.Templates
public abstract IEnumerable<string> ExpectedFilesAfterPublish { get; }
// Hook for subclasses to modify template immediately after "dotnet new". Typically used
// for temporary workarounds (e.g. changing TFM in csproj).
protected virtual void AfterNew(string tempDir) { }
// Hook for subclasses to normalize files after publish (e.g. replacing hash codes)
protected virtual IEnumerable<string> NormalizeFilesAfterPublish(IEnumerable<string> filesAfterPublish)
{
return filesAfterPublish;
}
private IEnumerable<string> GetObjFilesAfterRestore()
{
Directory.CreateDirectory(TempDir);
DotNetUtil.New(Name, TempDir);
AfterNew(TempDir);
DotNetUtil.Restore(TempDir, NuGetPackageSource, RuntimeIdentifier);
return IOUtil.GetFiles(Path.Combine(TempDir, "obj"));
}
private IEnumerable<string> GetObjFilesAfterRestoreIncremental()
{
// RestoreIncremental depends on Restore
_ = ObjFilesAfterRestore;
DotNetUtil.Restore(TempDir, NuGetPackageSource, RuntimeIdentifier);
return IOUtil.GetFiles(Path.Combine(TempDir, "obj"));
}
private (IEnumerable<string> ObjFiles, IEnumerable<string> BinFiles) GetFilesAfterBuild()
{
// Build depends on Restore
_ = ObjFilesAfterRestore;
// Build depends on RestoreIncremental
_ = ObjFilesAfterRestoreIncremental;
DotNetUtil.Build(TempDir, NuGetPackageSource, RuntimeIdentifier);
return (IOUtil.GetFiles(Path.Combine(TempDir, "obj")), IOUtil.GetFiles(Path.Combine(TempDir, "bin")));
}
private (IEnumerable<string> ObjFiles, IEnumerable<string> BinFiles) GetFilesAfterBuildIncremental()
{
// BuildIncremental depends on Build
_ = ObjFilesAfterBuild;
DotNetUtil.Build(TempDir, NuGetPackageSource, RuntimeIdentifier);
return (IOUtil.GetFiles(Path.Combine(TempDir, "obj")), IOUtil.GetFiles(Path.Combine(TempDir, "bin")));
@ -111,8 +156,17 @@ namespace AspNetCoreSdkTests.Templates
private IEnumerable<string> GetFilesAfterPublish()
{
// Publish depends on Build
_ = BinFilesAfterBuild;
// Publish depends on BuildIncremental
_ = BinFilesAfterBuildIncremental;
DotNetUtil.Publish(TempDir, RuntimeIdentifier);
return IOUtil.GetFiles(Path.Combine(TempDir, DotNetUtil.PublishOutput));
}
private IEnumerable<string> GetFilesAfterPublishIncremental()
{
// PublishIncremental depends on Publish
_ = FilesAfterPublish;
DotNetUtil.Publish(TempDir, RuntimeIdentifier);
return IOUtil.GetFiles(Path.Combine(TempDir, DotNetUtil.PublishOutput));
@ -120,16 +174,16 @@ namespace AspNetCoreSdkTests.Templates
private (HttpResponseMessage Http, HttpResponseMessage Https, string ServerOutput, string ServerError) GetHttpResponsesAfterRun()
{
// Run depends on Build
_ = BinFilesAfterBuild;
// Run depends on BuildIncremental
_ = BinFilesAfterBuildIncremental;
return GetHttpResponses(DotNetUtil.Run(TempDir, RuntimeIdentifier));
}
private (HttpResponseMessage Http, HttpResponseMessage Https, string ServerOutput, string ServerError) GetHttpResponsesAfterExec()
{
// Exec depends on Publish
_ = FilesAfterPublish;
// Exec depends on PublishIncremental
_ = FilesAfterPublishIncremental;
return GetHttpResponses(DotNetUtil.Exec(TempDir, Name, RuntimeIdentifier));
}

View File

@ -1,4 +1,5 @@
using System;
using AspNetCoreSdkTests.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -22,25 +23,18 @@ namespace AspNetCoreSdkTests.Templates
$"{Name}.RazorTargetAssemblyInfo.cache",
}.Select(p => Path.Combine(OutputPath, p)));
private IDictionary<RuntimeIdentifier, Func<IEnumerable<string>>> _additionalFilesAfterPublish =>
new Dictionary<RuntimeIdentifier, Func<IEnumerable<string>>>()
private IDictionary<(string TargetFrameworkMoniker, RuntimeIdentifier), Func<IEnumerable<string>>> _additionalFilesAfterPublish =>
new Dictionary<(string TargetFrameworkMoniker, RuntimeIdentifier), Func<IEnumerable<string>>>()
{
{ RuntimeIdentifier.None, () => new[]
{ ("netcoreapp2.1", RuntimeIdentifier.None), () => new[]
{
// Publish includes all *.config and *.json files (https://github.com/aspnet/websdk/issues/334)
"NuGet.config",
"web.config",
}
},
{ RuntimeIdentifier.Win_x64, () =>
_additionalFilesAfterPublish[RuntimeIdentifier.Linux_x64]()
.Concat(new[]
{
"sni.dll",
})
},
{ RuntimeIdentifier.Linux_x64, () =>
_additionalFilesAfterPublish[RuntimeIdentifier.None]()
{ ("netcoreapp2.1", RuntimeIdentifier.Linux_x64), () =>
_additionalFilesAfterPublish[("netcoreapp2.1", RuntimeIdentifier.None)]()
.Concat(new[]
{
"Microsoft.AspNetCore.Antiforgery.dll",
@ -211,11 +205,44 @@ namespace AspNetCoreSdkTests.Templates
"System.Threading.Channels.dll",
})
},
{ RuntimeIdentifier.OSX_x64, () => _additionalFilesAfterPublish[RuntimeIdentifier.Linux_x64]() },
{ ("netcoreapp2.1", RuntimeIdentifier.OSX_x64), () =>
_additionalFilesAfterPublish[("netcoreapp2.1", RuntimeIdentifier.Linux_x64)]()
},
{ ("netcoreapp2.1", RuntimeIdentifier.Win_x64), () =>
_additionalFilesAfterPublish[("netcoreapp2.1", RuntimeIdentifier.Linux_x64)]()
.Concat(new[]
{
"sni.dll",
})
},
{ ("netcoreapp2.2", RuntimeIdentifier.None), () =>
_additionalFilesAfterPublish[("netcoreapp2.1", RuntimeIdentifier.None)]()
},
{ ("netcoreapp2.2", RuntimeIdentifier.Linux_x64), () =>
_additionalFilesAfterPublish[("netcoreapp2.1", RuntimeIdentifier.Linux_x64)]()
.Concat(new[]
{
"Microsoft.AspNetCore.Diagnostics.HealthChecks.dll",
"Microsoft.AspNetCore.Server.IIS.dll",
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.dll",
"Microsoft.Extensions.Diagnostics.HealthChecks.dll",
})
},
{ ("netcoreapp2.2", RuntimeIdentifier.OSX_x64), () =>
_additionalFilesAfterPublish[("netcoreapp2.2", RuntimeIdentifier.Linux_x64)]()
},
{ ("netcoreapp2.2", RuntimeIdentifier.Win_x64), () =>
_additionalFilesAfterPublish[("netcoreapp2.2", RuntimeIdentifier.Linux_x64)]()
.Concat(new[]
{
"aspnetcorev2_inprocess.dll",
"sni.dll",
})
},
};
public override IEnumerable<string> ExpectedFilesAfterPublish =>
base.ExpectedFilesAfterPublish
.Concat(_additionalFilesAfterPublish[RuntimeIdentifier]());
.Concat(_additionalFilesAfterPublish[(DotNetUtil.TargetFrameworkMoniker, RuntimeIdentifier)]());
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.Internal;
using NuGet.Versioning;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -22,26 +23,36 @@ namespace AspNetCoreSdkTests.Util
// Bind to dynamic port 0 to avoid port conflicts during parallel tests
private const string _urls = "--urls http://127.0.0.1:0;https://127.0.0.1:0";
public static string PublishOutput => "pub";
// Must publish to folder under "bin" or "obj" to prevent double-copying publish output during incremental publish
public static string PublishOutput => Path.Combine("bin", "pub");
private static readonly Lazy<Version> _sdkVersion = new Lazy<Version>(GetSdkVersion, LazyThreadSafetyMode.PublicationOnly);
private static readonly Lazy<(SemanticVersion SdkVersion, SemanticVersion RuntimeVersion)> _versions =
new Lazy<(SemanticVersion SdkVersion, SemanticVersion RuntimeVersion)>(GetVersions, LazyThreadSafetyMode.PublicationOnly);
public static Version SdkVersion => _sdkVersion.Value;
public static SemanticVersion SdkVersion => _versions.Value.SdkVersion;
private static Version GetSdkVersion()
public static SemanticVersion RuntimeVersion => _versions.Value.RuntimeVersion;
public static string TargetFrameworkMoniker => $"netcoreapp{RuntimeVersion.Major}.{RuntimeVersion.Minor}";
private static (SemanticVersion SdkVersion, SemanticVersion RuntimeVersion) GetVersions()
{
var info = RunDotNet("--info", workingDirectory: null);
var versionString = Regex.Match(info, @"Version:\W*([0-9.]+)").Groups[1].Value;
var version = new Version(versionString);
// Supported version range is [2.1.300,2.1.401] (inclusive)
if (version >= new Version(2, 1, 300) && version <= new Version(2, 1, 401))
var sdkVersionString = Regex.Match(info, @"Version:\s*(\S+)").Groups[1].Value;
var sdkVersion = SemanticVersion.Parse(sdkVersionString);
var runtimeVersionString = Regex.Match(info, @"Microsoft.NETCore.App\s*(\S+)").Groups[1].Value;
var runtimeVersion = SemanticVersion.Parse(runtimeVersionString);
// Supported version range is [2.1.300,2.2.100] (inclusive)
if (sdkVersion >= new SemanticVersion(2, 1, 300) && sdkVersion <= new SemanticVersion(2, 2, 100))
{
return version;
return (sdkVersion, runtimeVersion);
}
else
{
throw new InvalidOperationException($"Unsupported SDK version: {version}");
throw new InvalidOperationException($"Unsupported SDK version: {sdkVersion}");
}
}
@ -74,7 +85,7 @@ namespace AspNetCoreSdkTests.Util
{
// "dotnet build" cannot use "--no-restore" if the app is self-contained and the SDK contains a patched runtime
// https://github.com/dotnet/sdk/issues/2312, https://github.com/dotnet/cli/issues/9514
bool restoreRequired = (runtimeIdentifier != RuntimeIdentifier.None) && (DotNetUtil.SdkVersion >= new Version(2, 1, 301));
bool restoreRequired = (runtimeIdentifier != RuntimeIdentifier.None) && (DotNetUtil.RuntimeVersion.Patch > 0);
var restoreArgument = restoreRequired ? $"--no-cache {packageSource.SourceArgument}" : "--no-restore";

View File

@ -8,6 +8,11 @@ namespace AspNetCoreSdkTests.Util
{
internal static class IOUtil
{
public static void ReplaceInFile(string path, string oldValue, string newValue)
{
File.WriteAllText(path, File.ReadAllText(path).Replace(oldValue, newValue));
}
public static IEnumerable<string> GetFiles(string path)
{
return Directory.GetFiles(path, "*", SearchOption.AllDirectories)
@ -28,6 +33,14 @@ namespace AspNetCoreSdkTests.Util
return temp;
}
public static void DeleteFileIfExists(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
public static void DeleteDir(string path)
{
// If delete fails (e.g. due to a file in use), retry once every second up to 20 times.