diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 44d56a3fb..d24fc5d42 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,6 +28,10 @@ jobs: - name: Build Coyote projects run: ./Scripts/build.ps1 shell: pwsh + - name: Validate Coyote Rewriting + if: ${{ matrix.platform == 'windows-latest' }} + run: ./Tests/compare-rewriting-diff-logs.ps1 + shell: pwsh - name: Run Coyote Tests run: ./Scripts/run-tests.ps1 shell: pwsh diff --git a/.gitignore b/.gitignore index d3fed231f..c28b72e61 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files +*.rsuser *.suo *.user *.userosscache @@ -12,25 +13,27 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Mono auto generated files +mono_crash.* + # Build results -[Bb]uild/ -[Cc]odegen/ [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ [Bb]in/ -[Bb]inaries/ [Oo]bj/ [Ll]og/ +[Ll]ogs/ -# Visual Studio cache/options directory +# Visual Studio 2015/2017 cache/options directory .vs/ -**/.vscode/** - # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ @@ -41,9 +44,10 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -52,13 +56,14 @@ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ -benchmark_*/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml @@ -66,7 +71,7 @@ StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c -*_i.h +*_h.h *.ilk *.meta *.obj @@ -83,7 +88,9 @@ StyleCopReport.xml *.tlh *.tmp *.tmp_proj +*_wpftmp.csproj *.log +*.tlog *.vspscc *.vssscc .builds @@ -125,9 +132,6 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding add-in -.JustCode - # TeamCity is a build add-in _TeamCity* @@ -138,6 +142,11 @@ _TeamCity* .axoCover/* !.axoCover/settings.json +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + # Visual Studio code coverage results *.coverage *.coveragexml @@ -183,8 +192,10 @@ publish/ # in these scripts will be unencrypted PublishScripts/ -# NuGet +# NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -194,7 +205,9 @@ PublishScripts/ # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets -nuget.exe + +# Nuget personal access tokens and Credentials +# nuget.config # Microsoft Azure Build Output csx/ @@ -210,12 +223,14 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx +*.appxbundle +*.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!*.[Cc]ache/ +!?*.[Cc]ache/ # Others ClientBin/ @@ -259,6 +274,9 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -294,12 +312,8 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ +# CodeRush personal settings +.cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ @@ -336,12 +350,47 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ -# mkdocs -site +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml # Website +site docs/assets/images/Thumbs.db docs/_data/sidebar-temp.yml docs/_learn/ref/toc.yml .ipynb_checkpoints/ -*-checkpoint.ipynb \ No newline at end of file +*-checkpoint.ipynb diff --git a/Scripts/build.ps1 b/Scripts/build.ps1 index 3f72a1a85..1f5cebc7f 100644 --- a/Scripts/build.ps1 +++ b/Scripts/build.ps1 @@ -4,7 +4,8 @@ param( [ValidateSet("Debug", "Release")] [string]$configuration = "Release", - [bool]$local = $true + [bool]$local = $true, + [switch]$latest ) $ScriptDir = $PSScriptRoot @@ -40,12 +41,12 @@ $solution = Join-Path -Path $ScriptDir -ChildPath "\.." -AdditionalChildPath "Co $command = "build -c $configuration $solution /p:Platform=""Any CPU""" if ($local) { - if ($version_net4) { + if ($version_net4 -and -not $latest) { # Build .NET Framework 4.x as well as the new version. $command = $command + " /p:BUILD_NET462=yes" } - if ($null -ne $version_netcore31 -and $version_netcore31 -ne $sdk_version) { + if ($null -ne $version_netcore31 -and $version_netcore31 -ne $sdk_version -and -not $latest) { # Build .NET Core 3.1 as well as the new version. $command = $command + " /p:BUILD_NETCORE31=yes" } diff --git a/Source/Test/Rewriting/CachedNameProvider.cs b/Source/Test/Rewriting/CachedNameProvider.cs index 468bfd4a2..8c966aba5 100644 --- a/Source/Test/Rewriting/CachedNameProvider.cs +++ b/Source/Test/Rewriting/CachedNameProvider.cs @@ -52,7 +52,7 @@ internal static class CachedNameProvider internal static string GenericHashSetFullName { get; } = typeof(SystemGenericCollections.HashSet<>).FullName; internal static string ConcurrentBagFullName { get; } = typeof(SystemConcurrentCollections.ConcurrentBag<>).FullName; - internal static string ConcurrentDictonaryFullName { get; } = typeof(SystemConcurrentCollections.ConcurrentDictionary<,>).FullName; + internal static string ConcurrentDictionaryFullName { get; } = typeof(SystemConcurrentCollections.ConcurrentDictionary<,>).FullName; internal static string ConcurrentQueueFullName { get; } = typeof(SystemConcurrentCollections.ConcurrentQueue<>).FullName; internal static string ConcurrentStackFullName { get; } = typeof(SystemConcurrentCollections.ConcurrentStack<>).FullName; } diff --git a/Source/Test/Rewriting/Passes/AssemblyDiffingPass.cs b/Source/Test/Rewriting/Passes/AssemblyDiffingPass.cs new file mode 100644 index 000000000..b651df857 --- /dev/null +++ b/Source/Test/Rewriting/Passes/AssemblyDiffingPass.cs @@ -0,0 +1,532 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Coyote.IO; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Microsoft.Coyote.Rewriting +{ + /// + /// A pass that diffs the IL contents of assemblies and returns them as JSON. + /// + internal class AssemblyDiffingPass : Pass + { + /// + /// Map from assemblies to IL contents. + /// + private Dictionary ContentMap; + + /// + /// Initializes a new instance of the class. + /// + internal AssemblyDiffingPass(IEnumerable visitedAssemblies, ILogger logger) + : base(visitedAssemblies, logger) + { + this.ContentMap = new Dictionary(); + } + + /// + protected internal override void VisitAssembly(AssemblyDefinition assembly) + { + if (!this.ContentMap.ContainsKey(assembly.FullName)) + { + var contents = new AssemblyContents() + { + FullName = assembly.FullName, + Modules = new List() + }; + + this.ContentMap.Add(assembly.FullName, contents); + } + + base.VisitAssembly(assembly); + } + + /// + protected internal override void VisitModule(ModuleDefinition module) + { + if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) + { + contents.Modules.Add(new ModuleContents() + { + Name = module.Name, + Types = new List() + }); + } + + base.VisitModule(module); + } + + /// + protected internal override void VisitType(TypeDefinition type) + { + if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) + { + contents.Modules.FirstOrDefault(m => m.Name == this.Module.Name)?.Types.Add( + new TypeContents() + { + FullName = type.FullName, + Methods = new List() + }); + } + + base.VisitType(type); + } + + /// + protected internal override void VisitField(FieldDefinition field) + { + if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) + { + contents.Modules.FirstOrDefault(m => m.Name == this.Module.Name)?.Types + .FirstOrDefault(t => t.FullName == this.TypeDef.FullName)? + .AddField(field); + } + + base.VisitField(field); + } + + /// + protected internal override void VisitMethod(MethodDefinition method) + { + if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) + { + contents.Modules.FirstOrDefault(m => m.Name == this.Module.Name)?.Types + .FirstOrDefault(t => t.FullName == this.TypeDef.FullName)?.Methods + .Add(new MethodContents() + { + Index = this.TypeDef.Methods.IndexOf(method), + Name = method.Name, + FullName = method.FullName, + Instructions = new List() + }); + } + + base.VisitMethod(method); + } + + /// + protected override void VisitVariable(VariableDefinition variable) + { + if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) + { + contents.Modules.FirstOrDefault(m => m.Name == this.Module.Name)?.Types + .FirstOrDefault(t => t.FullName == this.TypeDef.FullName)?.Methods + .FirstOrDefault(m => m.FullName == this.Method.FullName)? + .AddVariable(variable); + } + + base.VisitVariable(variable); + } + + /// + protected override Instruction VisitInstruction(Instruction instruction) + { + if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) + { + contents.Modules.FirstOrDefault(m => m.Name == this.Module.Name)?.Types + .FirstOrDefault(t => t.FullName == this.TypeDef.FullName)?.Methods + .FirstOrDefault(m => m.FullName == this.Method.FullName)?.Instructions + .Add(instruction.ToString()); + } + + return base.VisitInstruction(instruction); + } + + /// + /// Returns the diff between the IL contents of this pass against the specified pass as JSON. + /// + internal string GetDiffJson(AssemblyInfo assembly, AssemblyDiffingPass pass) + { + if (!this.ContentMap.TryGetValue(assembly.FullName, out AssemblyContents thisContents) || + !pass.ContentMap.TryGetValue(assembly.FullName, out AssemblyContents otherContents)) + { + this.Logger.WriteLine(LogSeverity.Error, "Unable to diff IL code that belongs to different assemblies."); + return string.Empty; + } + + // Diff contents of the two assemblies. + var diffedContents = thisContents.Diff(otherContents); + return this.GetJson(diffedContents); + } + + /// + /// Returns the IL contents of the specified assembly as JSON. + /// + internal string GetJson(AssemblyInfo assembly) => + this.ContentMap.TryGetValue(assembly.Definition.FullName, out AssemblyContents contents) ? + this.GetJson(contents) : string.Empty; + + /// + /// Returns the IL contents of the specified assembly as JSON. + /// + private string GetJson(AssemblyContents contents) + { + try + { + contents.Resolve(); + return JsonSerializer.Serialize(contents, new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true + }); + } + catch (Exception ex) + { + this.Logger.WriteLine(LogSeverity.Error, $"Unable to serialize IL to JSON. {ex.Message}"); + } + + return string.Empty; + } + + /// + /// The status of diffing two IL contents. + /// + private enum DiffStatus + { + /// + /// Contents are identical. + /// + None, + + /// + /// Contents have been added. + /// + Added, + + /// + /// Contents have been removed. + /// + Removed + } + + private class AssemblyContents + { + public string FullName { get; set; } + public List Modules { get; set; } + + /// + /// Returns the diff between the IL contents of this assembly against the specified assembly. + /// + internal AssemblyContents Diff(AssemblyContents other) + { + var diffedContents = new AssemblyContents() + { + FullName = this.FullName, + Modules = new List() + }; + + var diffedModules = this.Modules.Union(other.Modules); + foreach (var module in diffedModules) + { + var thisModule = this.Modules.FirstOrDefault(m => m.Equals(module)); + var otherModule = other.Modules.FirstOrDefault(m => m.Equals(module)); + if (thisModule is null) + { + diffedContents.Modules.Add(module.Clone(DiffStatus.Added)); + } + else if (otherModule is null) + { + diffedContents.Modules.Add(module.Clone(DiffStatus.Removed)); + } + else + { + diffedContents.Modules.Add(thisModule.Diff(otherModule)); + } + } + + return diffedContents; + } + + internal void Resolve() + { + this.Modules.ForEach(m => m.Resolve()); + this.Modules.RemoveAll(t => t.Types is null); + this.Modules = this.Modules.Count is 0 ? null : this.Modules; + } + + public override bool Equals(object obj) => obj is AssemblyContents other && this.FullName == other.FullName; + public override int GetHashCode() => this.FullName.GetHashCode(); + } + + private class ModuleContents + { + public string Name { get; set; } + public List Types { get; set; } + + [JsonIgnore] + internal DiffStatus DiffStatus { get; private set; } = DiffStatus.None; + + /// + /// Returns the diff between the IL contents of this module against the specified module. + /// + internal ModuleContents Diff(ModuleContents other) + { + var diffedContents = new ModuleContents() + { + Name = this.Name, + Types = new List() + }; + + var diffedTypes = this.Types.Union(other.Types); + foreach (var type in diffedTypes) + { + var thisType = this.Types.FirstOrDefault(t => t.Equals(type)); + var otherType = other.Types.FirstOrDefault(t => t.Equals(type)); + if (thisType is null) + { + diffedContents.Types.Add(type.Clone(DiffStatus.Added)); + } + else if (otherType is null) + { + diffedContents.Types.Add(type.Clone(DiffStatus.Removed)); + } + else + { + diffedContents.Types.Add(thisType.Diff(otherType)); + } + } + + return diffedContents; + } + + internal ModuleContents Clone(DiffStatus status) + { + string prefix = status is DiffStatus.Added ? "[+] " : status is DiffStatus.Removed ? "[-] " : string.Empty; + return new ModuleContents() + { + Name = prefix + this.Name, + Types = this.Types.Select(t => t.Clone(DiffStatus.None)).ToList(), + DiffStatus = status + }; + } + + internal void Resolve() + { + this.Types.ForEach(t => t.Resolve()); + this.Types.RemoveAll(t => t.Fields is null && t.Methods is null); + this.Types = this.Types.Count is 0 ? null : this.Types; + } + + public override bool Equals(object obj) => obj is ModuleContents other && this.Name == other.Name; + public override int GetHashCode() => this.Name.GetHashCode(); + } + + private class TypeContents + { + public string FullName { get; set; } + public IEnumerable Fields { get; set; } + public List Methods { get; set; } + + private List<(string, string)> FieldContents = new List<(string, string)>(); + + [JsonIgnore] + internal DiffStatus DiffStatus { get; private set; } = DiffStatus.None; + + internal void AddField(FieldDefinition field) + { + this.FieldContents.Add((field.Name, field.ToString())); + } + + /// + /// Returns the diff between the IL contents of this type against the specified type. + /// + internal TypeContents Diff(TypeContents other) + { + var diffedContents = new TypeContents() + { + FullName = this.FullName, + Methods = new List() + }; + + var diffedFields = this.FieldContents.Union(other.FieldContents); + foreach (var field in diffedFields) + { + var (thisField, thisType) = this.FieldContents.FirstOrDefault(f => f == field); + var (otherField, otherType) = other.FieldContents.FirstOrDefault(f => f == field); + if (thisField is null) + { + diffedContents.FieldContents.Add((field.Item1, "[+] " + field.Item2)); + } + else if (otherField is null) + { + diffedContents.FieldContents.Add((field.Item1, "[-] " + field.Item2)); + } + } + + var diffedMethods = this.Methods.Union(other.Methods); + foreach (var method in diffedMethods) + { + var thisMethod = this.Methods.FirstOrDefault(m => m.Equals(method)); + var otherMethod = other.Methods.FirstOrDefault(m => m.Equals(method)); + if (thisMethod is null) + { + diffedContents.Methods.Add(method.Clone(DiffStatus.Added)); + } + else if (otherMethod is null) + { + diffedContents.Methods.Add(method.Clone(DiffStatus.Removed)); + } + else + { + diffedContents.Methods.Add(thisMethod.Diff(otherMethod)); + } + } + + return diffedContents; + } + + internal TypeContents Clone(DiffStatus status) + { + string prefix = status is DiffStatus.Added ? "[+] " : status is DiffStatus.Removed ? "[-] " : string.Empty; + return new TypeContents() + { + FullName = prefix + this.FullName, + FieldContents = this.FieldContents.ToList(), + Methods = this.Methods.Select(m => m.Clone(DiffStatus.None)).ToList(), + DiffStatus = status + }; + } + + internal void Resolve() + { + this.FieldContents = this.FieldContents.Count is 0 ? null : this.FieldContents; + if (this.FieldContents != null) + { + this.Fields = this.FieldContents.OrderBy(kvp => kvp.Item1).Select(kvp => kvp.Item2); + } + + this.Methods.ForEach(m => m.Resolve()); + this.Methods.RemoveAll(m => m.Variables is null && m.Instructions is null); + this.Methods = this.Methods.Count is 0 ? null : this.Methods; + } + + public override bool Equals(object obj) => obj is TypeContents other && this.FullName == other.FullName; + public override int GetHashCode() => this.FullName.GetHashCode(); + } + + private class MethodContents + { + [JsonIgnore] + internal int Index { get; set; } + + public string Name { get; set; } + public string FullName { get; set; } + public IEnumerable Variables { get; set; } + public List Instructions { get; set; } + + private List<(int, string)> VariableContents = new List<(int, string)>(); + + [JsonIgnore] + internal DiffStatus DiffStatus { get; private set; } = DiffStatus.None; + + internal void AddVariable(VariableDefinition variable) + { + this.VariableContents.Add((variable.Index, $"[{variable.Index}] {variable.VariableType.FullName}")); + } + + /// + /// Returns the diff between the IL contents of this method against the specified method. + /// + internal MethodContents Diff(MethodContents other) + { + // TODO: show diff in method signature. + var diffedContents = new MethodContents() + { + Index = this.Index, + Name = this.Name, + FullName = other.FullName, + Instructions = new List() + }; + + var diffedVariables = this.VariableContents.Union(other.VariableContents); + foreach (var variable in diffedVariables) + { + var (thisVariable, thisType) = this.VariableContents.FirstOrDefault(v => v == variable); + var (otherVariable, otherType) = other.VariableContents.FirstOrDefault(v => v == variable); + if (thisType is null) + { + diffedContents.VariableContents.Add((variable.Item1, "[+] " + variable.Item2)); + } + else if (otherType is null) + { + diffedContents.VariableContents.Add((variable.Item1, "[-] " + variable.Item2)); + } + } + + var diffedInstructions = this.Instructions.Union(other.Instructions); + foreach (var instruction in diffedInstructions) + { + var thisInstruction = this.Instructions.FirstOrDefault(i => i == instruction); + var otherInstruction = other.Instructions.FirstOrDefault(i => i == instruction); + if (thisInstruction is null) + { + diffedContents.Instructions.Add("[+] " + instruction); + } + else if (otherInstruction is null) + { + diffedContents.Instructions.Add("[-] " + instruction); + } + } + + diffedContents.Instructions.Sort((x, y) => + { + // Find offset of each instruction and convert from hex to int. + // The offset in IL is in the form 'IL_004a'. + int xOffset = Convert.ToInt32(x.Substring(7, 4), 16); + int yOffset = Convert.ToInt32(y.Substring(7, 4), 16); + return xOffset == yOffset ? x[1].CompareTo(y[1]) * -1 : xOffset.CompareTo(yOffset); + }); + + return diffedContents; + } + + internal MethodContents Clone(DiffStatus status) + { + string prefix = status is DiffStatus.Added ? "[+] " : status is DiffStatus.Removed ? "[-] " : string.Empty; + return new MethodContents() + { + Index = this.Index, + Name = this.Name, + FullName = prefix + this.FullName, + VariableContents = this.VariableContents.ToList(), + Instructions = this.Instructions.ToList(), + DiffStatus = status + }; + } + + internal void Resolve() + { + this.VariableContents = this.VariableContents.Count is 0 ? null : this.VariableContents; + if (this.VariableContents != null) + { + this.Variables = this.VariableContents.OrderBy(kvp => kvp.Item1).Select(kvp => kvp.Item2); + } + + this.Instructions = this.Instructions.Count is 0 ? null : this.Instructions; + } + + public override bool Equals(object obj) => obj is MethodContents other && + this.Index == other.Index && this.Name == other.Name; + + public override int GetHashCode() + { + unchecked + { + var hash = 19; + hash = (hash * 31) + this.Index.GetHashCode(); + hash = (hash * 31) + this.Name.GetHashCode(); + return hash; + } + } + } + } +} diff --git a/Source/Test/Rewriting/Passes/LoggingPass.cs b/Source/Test/Rewriting/Passes/LoggingPass.cs deleted file mode 100644 index 422ae7c8e..000000000 --- a/Source/Test/Rewriting/Passes/LoggingPass.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Microsoft.Coyote.IO; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace Microsoft.Coyote.Rewriting -{ - /// - /// A pass that logs all assembly contents into text. - /// - internal class LoggingPass : Pass - { - /// - /// Map from assemblies to IL contents. - /// - private Dictionary ContentMap; - - /// - /// Initializes a new instance of the class. - /// - internal LoggingPass(IEnumerable visitedAssemblies, ILogger logger) - : base(visitedAssemblies, logger) - { - this.ContentMap = new Dictionary(); - } - - /// - internal override void VisitAssembly(AssemblyDefinition assembly) - { - if (!this.ContentMap.ContainsKey(assembly.FullName)) - { - var contents = new AssemblyContents() - { - AssemblyName = assembly.FullName, - Modules = new List() - }; - - this.ContentMap.Add(assembly.FullName, contents); - } - - base.VisitAssembly(assembly); - } - - /// - internal override void VisitModule(ModuleDefinition module) - { - if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) - { - contents.Modules.Add(new ModuleContents() - { - FileName = module.FileName, - Types = new List() - }); - } - - base.VisitModule(module); - } - - /// - internal override void VisitType(TypeDefinition type) - { - if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) - { - contents.Modules.FirstOrDefault(m => m.FileName == this.Module.FileName)?.Types.Add( - new TypeContents() - { - Type = type.FullName, - Fields = new List(), - Methods = new List() - }); - } - - base.VisitType(type); - } - - /// - internal override void VisitField(FieldDefinition field) - { - if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) - { - contents.Modules.FirstOrDefault(m => m.FileName == this.Module.FileName)?.Types - .FirstOrDefault(t => t.Type == this.TypeDef.FullName)?.Fields.Add(field.FullName); - } - - base.VisitField(field); - } - - /// - internal override void VisitMethod(MethodDefinition method) - { - if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) - { - contents.Modules.FirstOrDefault(m => m.FileName == this.Module.FileName)?.Types - .FirstOrDefault(t => t.Type == this.TypeDef.FullName)?.Methods - .Add(new MethodContents() - { - Method = method.FullName, - Variables = new List(), - Instructions = new List() - }); - } - - base.VisitMethod(method); - } - - /// - protected override void VisitVariable(VariableDefinition variable) - { - if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) - { - contents.Modules.FirstOrDefault(m => m.FileName == this.Module.FileName)?.Types - .FirstOrDefault(t => t.Type == this.TypeDef.FullName)?.Methods - .FirstOrDefault(m => m.Method == this.Method.FullName)?.Variables - .Add(variable.ToString()); - } - - base.VisitVariable(variable); - } - - /// - protected override Instruction VisitInstruction(Instruction instruction) - { - if (this.ContentMap.TryGetValue(this.Assembly.FullName, out AssemblyContents contents)) - { - contents.Modules.FirstOrDefault(m => m.FileName == this.Module.FileName)?.Types - .FirstOrDefault(t => t.Type == this.TypeDef.FullName)?.Methods - .FirstOrDefault(m => m.Method == this.Method.FullName)?.Instructions - .Add(instruction.ToString()); - } - - return base.VisitInstruction(instruction); - } - - /// - /// Returns the contents of the specified assembly as JSON. - /// - internal string GetJSON(AssemblyInfo assembly) - { - try - { - if (this.ContentMap.TryGetValue(assembly.Definition.FullName, out AssemblyContents contents)) - { - return JsonSerializer.Serialize(contents, new JsonSerializerOptions() - { - WriteIndented = true - }); - } - } - catch (Exception ex) - { - this.Logger.WriteLine(LogSeverity.Error, $"Unable to serialize IL to JSON. {ex.Message}"); - } - - return string.Empty; - } - - private class AssemblyContents - { - public string AssemblyName { get; set; } - public IList Modules { get; set; } - } - - private class ModuleContents - { - public string FileName { get; set; } - public IList Types { get; set; } - } - - private class TypeContents - { - public string Type { get; set; } - public IList Fields { get; set; } - public IList Methods { get; set; } - } - - private class MethodContents - { - public string Method { get; set; } - public IList Variables { get; set; } - public IList Instructions { get; set; } - } - } -} diff --git a/Source/Test/Rewriting/Passes/Pass.cs b/Source/Test/Rewriting/Passes/Pass.cs index 9680a036b..767ff1afb 100644 --- a/Source/Test/Rewriting/Passes/Pass.cs +++ b/Source/Test/Rewriting/Passes/Pass.cs @@ -69,7 +69,7 @@ protected Pass(IEnumerable visitedAssemblies, ILogger logger) /// Visits the specified . /// /// The assembly definition to visit. - internal virtual void VisitAssembly(AssemblyDefinition assembly) + protected internal virtual void VisitAssembly(AssemblyDefinition assembly) { this.Assembly = assembly; this.Module = null; @@ -83,7 +83,7 @@ internal virtual void VisitAssembly(AssemblyDefinition assembly) /// visited . /// /// The module definition to visit. - internal virtual void VisitModule(ModuleDefinition module) + protected internal virtual void VisitModule(ModuleDefinition module) { this.Module = module; this.TypeDef = null; @@ -96,7 +96,7 @@ internal virtual void VisitModule(ModuleDefinition module) /// visited . /// /// The type definition to visit. - internal virtual void VisitType(TypeDefinition type) + protected internal virtual void VisitType(TypeDefinition type) { this.TypeDef = type; this.Method = null; @@ -108,7 +108,7 @@ internal virtual void VisitType(TypeDefinition type) /// visited . /// /// The field definition to visit. - internal virtual void VisitField(FieldDefinition field) + protected internal virtual void VisitField(FieldDefinition field) { } @@ -117,7 +117,7 @@ internal virtual void VisitField(FieldDefinition field) /// visited . /// /// The method definition to visit. - internal virtual void VisitMethod(MethodDefinition method) + protected internal virtual void VisitMethod(MethodDefinition method) { this.Method = method; @@ -165,7 +165,7 @@ protected virtual Instruction VisitInstruction(Instruction instruction) /// /// Completes the visit over the current assembly. /// - internal virtual void CompleteVisit() + protected internal virtual void CompleteVisit() { } diff --git a/Source/Test/Rewriting/Passes/Rewriting/ConcurrentCollectionRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/ConcurrentCollectionRewritingPass.cs index 07d8567eb..bcaa8fec6 100644 --- a/Source/Test/Rewriting/Passes/Rewriting/ConcurrentCollectionRewritingPass.cs +++ b/Source/Test/Rewriting/Passes/Rewriting/ConcurrentCollectionRewritingPass.cs @@ -75,7 +75,7 @@ protected override TypeReference RewriteDeclaringTypeReference(MethodReference m { type = this.Module.ImportReference(typeof(ControlledConcurrentBag)); } - else if (fullName == CachedNameProvider.ConcurrentDictonaryFullName) + else if (fullName == CachedNameProvider.ConcurrentDictionaryFullName) { type = this.Module.ImportReference(typeof(ControlledConcurrentDictionary)); } diff --git a/Source/Test/Rewriting/Passes/Rewriting/ExceptionFilterRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/ExceptionFilterRewritingPass.cs index 6da8d597f..bf0a1967a 100644 --- a/Source/Test/Rewriting/Passes/Rewriting/ExceptionFilterRewritingPass.cs +++ b/Source/Test/Rewriting/Passes/Rewriting/ExceptionFilterRewritingPass.cs @@ -31,7 +31,7 @@ internal ExceptionFilterRewritingPass(IEnumerable visitedAssemblie } /// - internal override void VisitType(TypeDefinition type) + protected internal override void VisitType(TypeDefinition type) { this.IsAsyncStateMachineType = type.Interfaces.Any( i => i.InterfaceType.FullName == typeof(SystemCompiler.IAsyncStateMachine).FullName); @@ -39,7 +39,7 @@ internal override void VisitType(TypeDefinition type) } /// - internal override void VisitMethod(MethodDefinition method) + protected internal override void VisitMethod(MethodDefinition method) { base.VisitMethod(method); diff --git a/Source/Test/Rewriting/Passes/Rewriting/MSTestRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/MSTestRewritingPass.cs index cd84c40cf..064f51934 100644 --- a/Source/Test/Rewriting/Passes/Rewriting/MSTestRewritingPass.cs +++ b/Source/Test/Rewriting/Passes/Rewriting/MSTestRewritingPass.cs @@ -29,7 +29,7 @@ internal MSTestRewritingPass(Configuration configuration, IEnumerable - internal override void VisitMethod(MethodDefinition method) + protected internal override void VisitMethod(MethodDefinition method) { if (method.IsAbstract) { diff --git a/Source/Test/Rewriting/Passes/Rewriting/MonitorRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/MonitorRewritingPass.cs index 1539b4ce3..4de48fb3f 100644 --- a/Source/Test/Rewriting/Passes/Rewriting/MonitorRewritingPass.cs +++ b/Source/Test/Rewriting/Passes/Rewriting/MonitorRewritingPass.cs @@ -33,7 +33,7 @@ internal MonitorRewritingPass(IEnumerable visitedAssemblies, ILogg } /// - internal override void VisitModule(ModuleDefinition module) + protected internal override void VisitModule(ModuleDefinition module) { this.ControlledMonitorType = null; base.VisitModule(module); diff --git a/Source/Test/Rewriting/Passes/Rewriting/TaskRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/TaskRewritingPass.cs index ea9836dc8..16d9481c8 100644 --- a/Source/Test/Rewriting/Passes/Rewriting/TaskRewritingPass.cs +++ b/Source/Test/Rewriting/Passes/Rewriting/TaskRewritingPass.cs @@ -40,7 +40,7 @@ internal TaskRewritingPass(IEnumerable visitedAssemblies, ILogger } /// - internal override void VisitField(FieldDefinition field) + protected internal override void VisitField(FieldDefinition field) { if (this.TryRewriteCompilerType(field.FieldType, out TypeReference newFieldType)) { @@ -51,7 +51,7 @@ internal override void VisitField(FieldDefinition field) } /// - internal override void VisitMethod(MethodDefinition method) + protected internal override void VisitMethod(MethodDefinition method) { base.VisitMethod(method); diff --git a/Source/Test/Rewriting/Passes/Rewriting/ThreadingRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/ThreadingRewritingPass.cs index 2af0537ae..77b516d9e 100644 --- a/Source/Test/Rewriting/Passes/Rewriting/ThreadingRewritingPass.cs +++ b/Source/Test/Rewriting/Passes/Rewriting/ThreadingRewritingPass.cs @@ -25,7 +25,7 @@ internal ThreadingRewritingPass(IEnumerable visitedAssemblies, ILo } /// - internal override void VisitModule(ModuleDefinition module) + protected internal override void VisitModule(ModuleDefinition module) { this.ControlledThreadType = null; base.VisitModule(module); diff --git a/Source/Test/Rewriting/RewritingEngine.cs b/Source/Test/Rewriting/RewritingEngine.cs index ebea3dd56..5b2e0e4c8 100644 --- a/Source/Test/Rewriting/RewritingEngine.cs +++ b/Source/Test/Rewriting/RewritingEngine.cs @@ -158,11 +158,11 @@ private void InitializePasses(IEnumerable assemblies) this.Passes.AddLast(new InterAssemblyInvocationRewritingPass(assemblies, this.Logger)); this.Passes.AddLast(new UncontrolledInvocationRewritingPass(assemblies, this.Logger)); - if (this.Options.IsLoggingContentsAsJson) + if (this.Options.IsLoggingAssemblyContents || this.Options.IsDiffingAssemblyContents) { - // Logging the contents of an assembly must happen before and after any other pass. - this.Passes.AddFirst(new LoggingPass(assemblies, this.Logger)); - this.Passes.AddLast(new LoggingPass(assemblies, this.Logger)); + // Parsing the contents of an assembly must happen before and after any other pass. + this.Passes.AddFirst(new AssemblyDiffingPass(assemblies, this.Logger)); + this.Passes.AddLast(new AssemblyDiffingPass(assemblies, this.Logger)); } } @@ -190,14 +190,21 @@ private void RewriteAssembly(AssemblyInfo assembly, string outputPath) assembly.ApplyRewritingSignatureAttribute(GetAssemblyRewriterVersion()); // Write the binary in the output path with portable symbols enabled. - this.Logger.WriteLine($"..... Writing the modified '{assembly.Name}' assembly to " + - $"{(this.Options.IsReplacingAssemblies() ? assembly.FilePath : outputPath)}"); + string resolvedOutputPath = this.Options.IsReplacingAssemblies() ? assembly.FilePath : outputPath; + this.Logger.WriteLine($"..... Writing the modified '{assembly.Name}' assembly to {resolvedOutputPath}"); assembly.Write(outputPath); - if (this.Options.IsLoggingContentsAsJson) + if (this.Options.IsLoggingAssemblyContents) { - // Log the original and rewritten contents as JSON. - this.LogContentsToJson(assembly, outputPath); + // Write the IL before and after rewriting to a JSON file. + this.WriteILToJson(assembly, false, resolvedOutputPath); + this.WriteILToJson(assembly, true, resolvedOutputPath); + } + + if (this.Options.IsDiffingAssemblyContents) + { + // Write the IL diff before and after rewriting to a JSON file. + this.WriteILDiffToJson(assembly, resolvedOutputPath); } } finally @@ -218,6 +225,45 @@ private void RewriteAssembly(AssemblyInfo assembly, string outputPath) } } + /// + /// Writes the original or rewritten IL to a JSON file in the specified output path. + /// + internal void WriteILToJson(AssemblyInfo assembly, bool isRewritten, string outputPath) + { + var diffingPass = (isRewritten ? this.Passes.Last : this.Passes.First).Value as AssemblyDiffingPass; + if (diffingPass != null) + { + string json = diffingPass.GetJson(assembly); + if (!string.IsNullOrEmpty(json)) + { + string jsonFile = Path.ChangeExtension(outputPath, $".{(isRewritten ? "rw" : "il")}.json"); + this.Logger.WriteLine($"..... Writing the {(isRewritten ? "rewritten" : "original")} IL " + + $"of '{assembly.Name}' as JSON to {jsonFile}"); + File.WriteAllText(jsonFile, json); + } + } + } + + /// + /// Writes the IL diff to a JSON file in the specified output path. + /// + internal void WriteILDiffToJson(AssemblyInfo assembly, string outputPath) + { + var originalDiffingPass = this.Passes.First.Value as AssemblyDiffingPass; + var rewrittenDiffingPass = this.Passes.Last.Value as AssemblyDiffingPass; + if (originalDiffingPass != null && rewrittenDiffingPass != null) + { + // Compute the diff between the original and rewritten IL and dump it to JSON. + string diffJson = originalDiffingPass.GetDiffJson(assembly, rewrittenDiffingPass); + if (!string.IsNullOrEmpty(diffJson)) + { + string jsonFile = Path.ChangeExtension(outputPath, ".diff.json"); + this.Logger.WriteLine($"..... Writing the IL diff of '{assembly.Name}' as JSON to {jsonFile}"); + File.WriteAllText(jsonFile, diffJson); + } + } + } + /// /// Checks if the specified assembly has been already rewritten with the current version. /// @@ -232,29 +278,6 @@ public static bool IsAssemblyRewritten(Assembly assembly) => /// private static Version GetAssemblyRewriterVersion() => Assembly.GetExecutingAssembly().GetName().Version; - /// - /// Writes the contents before and after rewriting in JSON format to the specified output path. - /// - internal void LogContentsToJson(AssemblyInfo assembly, string outputPath) - { - outputPath = this.Options.IsReplacingAssemblies() ? assembly.FilePath : outputPath; - if (this.Passes.First.Value is LoggingPass originalLoggingPass) - { - string json = originalLoggingPass.GetJSON(assembly); - string jsonFile = Path.ChangeExtension(outputPath, ".il.json"); - this.Logger.WriteLine($"..... Writing the original IL of '{assembly.Name}' as JSON to {jsonFile}"); - File.WriteAllText(jsonFile, json); - } - - if (this.Passes.Last.Value is LoggingPass rewrittenLoggingPass) - { - string json = rewrittenLoggingPass.GetJSON(assembly); - string jsonFile = Path.ChangeExtension(outputPath, ".rw.json"); - this.Logger.WriteLine($"..... Writing the rewritten IL of '{assembly.Name}' as JSON to {jsonFile}"); - File.WriteAllText(jsonFile, json); - } - } - /// /// Creates the output directory, if it does not already exists, and copies all necessary files. /// diff --git a/Source/Test/Rewriting/RewritingOptions.cs b/Source/Test/Rewriting/RewritingOptions.cs index 53c68586e..146b42b4c 100644 --- a/Source/Test/Rewriting/RewritingOptions.cs +++ b/Source/Test/Rewriting/RewritingOptions.cs @@ -81,28 +81,22 @@ internal class RewritingOptions /// /// True if rewriting of unit test methods is enabled, else false. /// - /// - /// If unit test rewriting is enabled, Coyote will instrument the binary to run unit test - /// methods in the scope of the Coyote testing engine. Note that this rewriting does not - /// change the semantics of the original test. For example, if the test is sequential it - /// will remain sequential, limiting the concurrency coverage that Coyote can achieve. - /// internal bool IsRewritingUnitTests { get; set; } /// /// True if rewriting threads as controlled tasks. /// - /// - /// Normally Thread is not supported by Coyote, but this experimental feature wraps the - /// thread in a Task so that Coyote knows about it which avoids uncontrolled concurrency - /// errors in some cases. - /// internal bool IsRewritingThreads { get; set; } /// - /// True if the rewriter should log the IL contents before and after rewriting in JSON. + /// True if the rewriter should log the IL before and after rewriting. + /// + internal bool IsLoggingAssemblyContents { get; set; } + + /// + /// True if the rewriter should diff the IL before and after rewriting. /// - internal bool IsLoggingContentsAsJson { get; set; } + internal bool IsDiffingAssemblyContents { get; set; } /// /// The .NET platform version that Coyote was compiled for. @@ -147,7 +141,8 @@ internal static RewritingOptions Create() => IsRewritingDependencies = false, IsRewritingUnitTests = false, IsRewritingThreads = false, - IsLoggingContentsAsJson = false, + IsLoggingAssemblyContents = false, + IsDiffingAssemblyContents = false, }; /// diff --git a/Tests/Tests.Actors.BugFinding/Tests.Actors.BugFinding.csproj b/Tests/Tests.Actors.BugFinding/Tests.Actors.BugFinding.csproj index 4b7e24b7d..6d7c94378 100644 --- a/Tests/Tests.Actors.BugFinding/Tests.Actors.BugFinding.csproj +++ b/Tests/Tests.Actors.BugFinding/Tests.Actors.BugFinding.csproj @@ -30,9 +30,9 @@ - + - + \ No newline at end of file diff --git a/Tests/Tests.BugFinding/Tests.BugFinding.csproj b/Tests/Tests.BugFinding/Tests.BugFinding.csproj index fd9ee8ba6..988865e4e 100644 --- a/Tests/Tests.BugFinding/Tests.BugFinding.csproj +++ b/Tests/Tests.BugFinding/Tests.BugFinding.csproj @@ -26,9 +26,9 @@ - + - + \ No newline at end of file diff --git a/Tests/Tests.Rewriting/Tests.Rewriting.csproj b/Tests/Tests.Rewriting/Tests.Rewriting.csproj index eff823771..e5a6c3025 100644 --- a/Tests/Tests.Rewriting/Tests.Rewriting.csproj +++ b/Tests/Tests.Rewriting/Tests.Rewriting.csproj @@ -33,9 +33,9 @@ - + - + \ No newline at end of file diff --git a/Tests/Tests.Standalone/Tests.Standalone.csproj b/Tests/Tests.Standalone/Tests.Standalone.csproj index a5a5713b7..55f1971da 100644 --- a/Tests/Tests.Standalone/Tests.Standalone.csproj +++ b/Tests/Tests.Standalone/Tests.Standalone.csproj @@ -25,9 +25,9 @@ - + - + \ No newline at end of file diff --git a/Tests/compare-rewriting-diff-logs.ps1 b/Tests/compare-rewriting-diff-logs.ps1 new file mode 100644 index 000000000..be8f9f0c4 --- /dev/null +++ b/Tests/compare-rewriting-diff-logs.ps1 @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module $PSScriptRoot/../Scripts/powershell/common.psm1 -Force + +$framework = "net5.0" +$targets = [ordered]@{ + "rewriting" = "Tests.Rewriting" + "testing" = "Tests.BugFinding" + "actors" = "Tests.Actors" + "actors-testing" = "Tests.Actors.BugFinding" + "standalone" = "Tests.Standalone" +} + +$expected_hashes = [ordered]@{ + "rewriting" = "611BDAE4DE1DE025116805076A3BC33151BB1ABF1E369D6BEA819345C7BE8818" + "testing" = "471916B83D10B7665D775817ECBCFCAE129B4859BE147DF414976420920C67C7" + "actors" = "8A9EAED0963801134DC58273FAF30550482842860FBA52187942601F73FF4110" + "actors-testing" = "88865B28250700738C4E9A0A7463E8447272C031A4D256C6026D26C92C98240A" + "standalone" = "ABF26E1DC4CB7F3A65B4508229AF2DDDF896F3E351F3F4A928C86758F55742E0" +} + +Write-Comment -prefix "." -text "Comparing the test rewriting diff logs" -color "yellow" + +# Compare all IL diff logs. +foreach ($kvp in $targets.GetEnumerator()) { + $project = $($kvp.Value) + if ($project -eq $targets["actors"]) { + $project = $targets["actors-testing"] + } + + $new = "$PSScriptRoot/$project/bin/$framework/Microsoft.Coyote.$($kvp.Value).diff.json" + $new_hash = $(Get-FileHash $new).Hash + Write-Comment -prefix "..." -text "Computed IL diff hash '$new_hash' for '$($kvp.Value)' project" -color "white" + $expected_hash = $expected_hashes[$($kvp.Key)] + if ($new_hash -ne $expected_hash) { + Write-Error "The '$($kvp.Value)' project's IL diff hash '$new_hash' is not the expected '$expected_hash'." + exit 1 + } +} + +Write-Comment -prefix "." -text "Done" -color "green" diff --git a/Tools/Coyote/Utilities/CommandLineOptions.cs b/Tools/Coyote/Utilities/CommandLineOptions.cs index 14d178cf3..62553947f 100644 --- a/Tools/Coyote/Utilities/CommandLineOptions.cs +++ b/Tools/Coyote/Utilities/CommandLineOptions.cs @@ -70,7 +70,8 @@ internal CommandLineOptions() rewritingGroup.AddArgument("rewrite-dependencies", null, "Rewrite all dependent assemblies that are found in the same location as the given path", typeof(bool)); rewritingGroup.AddArgument("rewrite-unit-tests", null, "Rewrite unit tests to run in the scope of the Coyote testing engine", typeof(bool)); rewritingGroup.AddArgument("rewrite-threads", null, "Rewrite low-level threading APIs (experimental)", typeof(bool)); - rewritingGroup.AddArgument("dump-il", null, "Dumps the IL contents before and after rewriting as JSON", typeof(bool)); + rewritingGroup.AddArgument("dump-il", null, "Dumps the original and rewritten IL in JSON", typeof(bool)); + rewritingGroup.AddArgument("dump-il-diff", null, "Dumps the IL diff in JSON", typeof(bool)); var coverageGroup = this.Parser.GetOrCreateGroup("coverageGroup", "Code and activity coverage options"); coverageGroup.DependsOn = new CommandLineArgumentDependency() { Name = "command", Value = "test" }; @@ -418,7 +419,10 @@ private static void UpdateConfigurationWithParsedArgument(Configuration configur rewritingOptions.IsRewritingThreads = true; break; case "dump-il": - rewritingOptions.IsLoggingContentsAsJson = true; + rewritingOptions.IsLoggingAssemblyContents = true; + break; + case "dump-il-diff": + rewritingOptions.IsDiffingAssemblyContents = true; break; case "timeout-delay": configuration.TimeoutDelay = (uint)option.Value;