Skip to content

Commit

Permalink
Merge pull request #20 from jarlef/feature/early-exit-on-unchanged-input
Browse files Browse the repository at this point in the history
CLI: Add checksum calculation to avoid unecessary processing when generating code
  • Loading branch information
byme8 authored Dec 5, 2022
2 parents 52e6f1c + 0e7f77b commit 6b00048
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 13 deletions.
65 changes: 64 additions & 1 deletion src/Benchmarks/ZeroQL.Benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
using System.Text.Json.Serialization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using CliFx.Infrastructure;
using GraphQL.TestServer;
using Microsoft.Extensions.DependencyInjection;
using StrawberryShake.TestServerClient;
using ZeroQL;
using ZeroQL.CLI.Commands;

var serverContext = new ZeroQL.TestServer.Program.ServerContext();

Expand All @@ -26,7 +28,20 @@
return;
}

BenchmarkRunner.Run<RawVSZeroQL>();
if (!File.Exists(Generation.SchemaFile))
{
var path = new Uri(Generation.SchemaFile).AbsolutePath;
Console.WriteLine($"Unable to find schema file: {path}");
return;
}

var switcher = new BenchmarkSwitcher(new[] {
typeof(RawVSZeroQL),
typeof(Generation),
});

switcher.Run(args);


ZeroQL.TestServer.Program.StopServer(serverContext);

Expand Down Expand Up @@ -110,6 +125,54 @@ public async Task<int> ZeroQLRequestUpload()
}
}

public class Generation
{
public const string SchemaFile = "../../TestApp/ZeroQL.TestApp/schema.graphql";
public const string OutputFile = "./bin/GraphQL.g.cs";


[GlobalSetup]
public void BeforeBenchmark() {

if (!File.Exists(OutputFile))
{
return;
}

File.Delete(OutputFile);
}

[Benchmark]
public async Task GenerateWithoutChecksumOptimization()
{
using var console = new FakeInMemoryConsole();
var generateCommand = new GenerateCommand
{
Schema = SchemaFile,
Output = OutputFile,
Namespace = "GraphQL.Example",
ClientName = "TestClient",
Force = true,
};

await generateCommand.ExecuteAsync(console);
}

[Benchmark]
public async Task GenerateWithChecksumOptimization()
{
using var console = new FakeInMemoryConsole();
var generateCommand = new GenerateCommand
{
Schema = SchemaFile,
Output = OutputFile,
Namespace = "GraphQL.Example",
ClientName = "TestClient"
};

await generateCommand.ExecuteAsync(console);
}
}
public record GetMeQuery : GraphQL<Query, string>
{
public override string Execute(Query query)
Expand Down
1 change: 1 addition & 0 deletions src/Benchmarks/ZeroQL.Benchmark/ZeroQL.Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<ProjectReference Include="..\..\TestApp\ZeroQL.TestApp\ZeroQL.TestApp.csproj" />
<ProjectReference Include="..\..\ZeroQL.CLI\ZeroQL.CLI.csproj" />
<ProjectReference Include="..\..\ZeroQL.TestServer\ZeroQL.TestServer.csproj" />
<ProjectReference Include="..\StrawberryShake.Client\StrawberryShake.Client.csproj" />
<ProjectReference Include="..\..\ZeroQL.SourceGenerators\ZeroQL.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
Expand Down
1 change: 1 addition & 0 deletions src/TestApp/ZeroQL.TestApp/Generated/GraphQL.g.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file generated for ZeroQL.
// <auto-generated/>
// <checksum>43c7ab8d947a9ac9dde455d1b15fc2ca</checksum>
#pragma warning disable 8618
#nullable enable
namespace GraphQL.TestServer
Expand Down
28 changes: 25 additions & 3 deletions src/ZeroQL.CLI/Commands/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZeroQL.Bootstrap;
using ZeroQL.Internal;

namespace ZeroQL.CLI.Commands;

Expand All @@ -19,6 +20,9 @@ public class GenerateCommand : ICommand

[CommandOption("output", 'o', Description = "The output file. For example, './Generated/GraphQL.g.cs'", IsRequired = true)]
public string Output { get; set; }

[CommandOption("force", 'f', Description = "Ignore checksum check and generate source code", IsRequired = false)]
public bool Force { get; set; }

public async ValueTask ExecuteAsync(IConsole console)
{
Expand All @@ -35,14 +39,32 @@ public async ValueTask ExecuteAsync(IConsole console)
return;
}

var options = new GraphQlGeneratorOptions(Namespace)
{
ClientName = ClientName
};

if (!Force && File.Exists(Output))
{
var checksumFile = ChecksumHelper.GenerateChecksumFromSchemaFile(Schema, options);
var checksumSourceCode = ChecksumHelper.ExtractChecksumFromSourceCode(Output);

if (checksumFile == checksumSourceCode)
{
await console.Output.WriteLineAsync("The source code is up-to-date with graphql schema. Skipping code generation.");
return;
}
}

var graphql = await File.ReadAllTextAsync(Schema);
var csharpClient = GraphQLGenerator.ToCSharp(graphql, Namespace, ClientName);
var outputFolder = Path.GetDirectoryName(Output)!;
var csharpClient = GraphQLGenerator.ToCSharp(graphql, options);
var outputPath = Path.IsPathRooted(Output) ? Output : Path.GetFullPath(Output);
var outputFolder = Path.GetDirectoryName(outputPath)!;
if (!Directory.Exists(outputFolder))
{
Directory.CreateDirectory(outputFolder);
}

await File.WriteAllTextAsync(Output, csharpClient);
await File.WriteAllTextAsync(outputPath, csharpClient);
}
}
59 changes: 53 additions & 6 deletions src/ZeroQL.Tests/CLI/CLITests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using FluentAssertions;
using ZeroQL.Tests.Core;
using Xunit;
using Xunit.Abstractions;
using ZeroQL.CLI.Commands;
using ZeroQL.Tests.Data;

Expand All @@ -10,20 +11,66 @@ namespace ZeroQL.Tests.CLI;
public class CLITests
{
[Fact]
public async Task Generate()
public async Task Generate_CodeShouldCompile()
{
using var console = new FakeInMemoryConsole();
var generateCommand = new GenerateCommand();
generateCommand.Schema = "../../../../TestApp/ZeroQL.TestApp/schema.graphql";
generateCommand.Namespace = "GraphQL.TestServer";
generateCommand.ClientName = "TestServerClient";
generateCommand.Output = "../../../../TestApp/ZeroQL.TestApp/Generated/GraphQL.g.cs";

var generateCommand = new GenerateCommand
{
Schema = "../../../../TestApp/ZeroQL.TestApp/schema.graphql",
Namespace = "GraphQL.TestServer",
ClientName = "TestServerClient",
Output = "../../../../TestApp/ZeroQL.TestApp/Generated/GraphQL.g.cs",
Force = true
};

await generateCommand.ExecuteAsync(console);

console.ReadErrorString().Should().BeEmpty();
await TestProject.Project.CompileToRealAssembly();
}

[Fact]
public async Task Generate_ShouldNotGenerateCodeIfNotNeeded()
{
var outputFile = "GraphQL.g.cs";

if (File.Exists(outputFile))
{
File.Delete(outputFile);
}

using var console = new FakeInMemoryConsole();
var generateCommand = new GenerateCommand
{
Schema = "../../../../TestApp/ZeroQL.TestApp/schema.graphql",
Namespace = "GraphQL.TestServer",
ClientName = "TestServerClient",
Output = outputFile,
};


// should generate file the first time
await generateCommand.ExecuteAsync(console);

File.Exists(outputFile).Should().BeTrue();
console.ReadErrorString().Should().BeEmpty();
var lastWriteTime = File.GetLastWriteTime(outputFile);
console.Clear();

// nothing changed. should skip generation
await generateCommand.ExecuteAsync(console);
File.Exists(outputFile).Should().BeTrue();
File.GetLastWriteTime(outputFile).Should().Be(lastWriteTime);
console.ReadOutputString().Should().Contain("The source code is up-to-date with graphql schema. Skipping code generation.");
console.Clear();

// setting updated. should trigger new code generation
generateCommand.ClientName = "UpdatedClientName";
await generateCommand.ExecuteAsync(console);
File.Exists(outputFile).Should().BeTrue();
File.GetLastWriteTime(outputFile).Should().BeAfter(lastWriteTime);
}

[Fact]
public async Task Extract()
Expand Down
18 changes: 15 additions & 3 deletions src/ZeroQL.Tools/Bootstrap/GraphQLGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@ namespace ZeroQL.Bootstrap;

public static class GraphQLGenerator
{

public static string ToCSharp(string graphql, string clientNamespace, string? clientName)
{
var options = new GraphQlGeneratorOptions(clientNamespace)
{
ClientName = clientName
};

return ToCSharp(graphql, options);
}

public static string ToCSharp(string graphql, GraphQlGeneratorOptions options)
{
var schema = Parser.Parse(graphql);
var enums = schema.Definitions
Expand Down Expand Up @@ -64,8 +73,8 @@ public static string ToCSharp(string graphql, string clientNamespace, string? cl
.Select(o => CreateInterfaceDefinition(context, o))
.ToArray();

var namespaceDeclaration = NamespaceDeclaration(IdentifierName(clientNamespace));
var clientDeclaration = new[] { GenerateClient(clientName, queryType, mutationType) };
var namespaceDeclaration = NamespaceDeclaration(IdentifierName(options.ClientNamespace));
var clientDeclaration = new[] { GenerateClient(options.ClientName, queryType, mutationType) };
var typesDeclaration = GenerateTypes(types, queryType, mutationType);
var interfacesDeclaration = GenerateInterfaces(interfaces);
var inputsDeclaration = GenerateInputs(inputs);
Expand Down Expand Up @@ -93,10 +102,13 @@ public static string ToCSharp(string graphql, string clientNamespace, string? cl
"ZeroQL"
};

var checksum = ChecksumHelper.GenerateChecksumFromInlineSchema(graphql, options);

namespaceDeclaration = namespaceDeclaration
.WithLeadingTrivia(
Comment("// This file generated for ZeroQL."),
Comment("// <auto-generated/>"),
Comment($"// <checksum>{checksum}</checksum>"),
Trivia(disableWarning),
CarriageReturnLineFeed,
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)),
Expand Down
7 changes: 7 additions & 0 deletions src/ZeroQL.Tools/Bootstrap/GraphQlGeneratorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ZeroQL.Bootstrap;

public record GraphQlGeneratorOptions(string ClientNamespace)
{
public string ClientNamespace { get; } = ClientNamespace;
public string? ClientName { get; set; }
}
94 changes: 94 additions & 0 deletions src/ZeroQL.Tools/Internal/ChecksumHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using ZeroQL.Bootstrap;

namespace ZeroQL.Internal;

public class ChecksumHelper
{
/// <summary>
/// Calculate a MD5 checksum from a schema string
/// </summary>
/// <param name="schemaFile">The schema file</param>
/// <param name="options">The generator options</param>
/// <returns></returns>
public static string GenerateChecksumFromSchemaFile(string schemaFile, GraphQlGeneratorOptions options)
{
using var md5 = MD5.Create();
using var stream = File.OpenRead(schemaFile);
var checksum = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", String.Empty).ToLower();
return AppendOptionsToChecksum(checksum, options);
}

/// <summary>
/// Calculate a MD5 checksum from a schema string
/// </summary>
/// <param name="schema">The schema definition</param>
/// <param name="options">The generator options</param>
/// <returns></returns>
public static string GenerateChecksumFromInlineSchema(string schema, GraphQlGeneratorOptions options)
{
var checksum = GetHashFromString(schema);
return AppendOptionsToChecksum(checksum, options);
}

/// <summary>
/// Fetches the stored checksum from a previous generated source code file
/// </summary>
/// <param name="file">The file to fetch the checksum from</param>
/// <returns></returns>
public static string? ExtractChecksumFromSourceCode(string file)
{
var regex = new Regex(@"<checksum>(?<checksum>\w+)<\/checksum>");
foreach (var line in File.ReadLines(file))
{
if (!line.StartsWith("//"))
{
break;
}

var match = regex.Match(line);
if (match.Success)
{
return match.Groups["checksum"].Value;
}
}

return null;
}

/// <summary>
/// Append options affects the code to be outputted and needs to be a part of the calculated checksum
/// </summary>
/// <param name="checksumHash">The checksum based on graphql schema</param>
/// <param name="options">The generator options</param>
/// <returns></returns>
private static string AppendOptionsToChecksum(string checksumHash, GraphQlGeneratorOptions options)
{
var serializedOptions = JsonSerializer.Serialize(options);
var optionsChecksumHash = GetHashFromString(serializedOptions);

// Also append the version of ZeroQL.CLI into the checksum
// since updated tooling may affect the source code generated
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
return GetHashFromString(checksumHash + optionsChecksumHash + version);
}

/// <summary>
/// Generates a MD5 checksum based on a input string
/// </summary>
/// <param name="value">The value to generate a checksum for</param>
/// <returns>The MD5 checksum hash</returns>
private static string GetHashFromString(string value)
{
using var md5 = MD5.Create();
var hash = BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(value))).Replace("-", String.Empty).ToLower();
return hash;
}

}

0 comments on commit 6b00048

Please sign in to comment.