Skip to content

Commit

Permalink
Update the cache web api call
Browse files Browse the repository at this point in the history
  • Loading branch information
costin-zaharia-sonarsource committed Jan 24, 2023
1 parent 15f6a43 commit ad740c7
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 75 deletions.
40 changes: 21 additions & 19 deletions Tests/SonarScanner.MSBuild.PreProcessor.Test/CacheProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,7 @@ public async Task Execute_PullRequest_FullProcessing_WithCache()
public void ProcessPullRequest_EmptyCache_DoesNotProduceOutput()
{
using var sut = CreateSut();
var cache = new AnalysisCacheMsg();
sut.ProcessPullRequest(cache);
sut.ProcessPullRequest(Array.Empty<SensorCacheEntry>());

sut.UnchangedFilesPath.Should().BeNull();
logger.AssertInfoLogged("Incremental PR analysis: 0 files out of 0 are unchanged.");
Expand Down Expand Up @@ -253,19 +252,17 @@ public void ProcessPullRequest_SomeFilesChanged_ProducesOnlyUnchangedFiles()
public void ProcessPullRequest_UnexpectedCacheKeys()
{
using var sut = CreateSut(Mock.Of<IBuildSettings>(x => x.SonarScannerWorkingDirectory == @"C:\ValidBasePath"));
var cache = new AnalysisCacheMsg
{
Map =
{
{ new(Path.GetInvalidFileNameChars()), ByteString.Empty},
{new(Path.GetInvalidPathChars()), ByteString.Empty},
{string.Empty, ByteString.Empty},
{" ", ByteString.Empty},
{"\t", ByteString.Empty},
{"\n", ByteString.Empty},
{ "\r", ByteString.Empty }
}
};
var cache = new SensorCacheEntry[]
{
new() { Key = new(Path.GetInvalidFileNameChars()), Data = ByteString.Empty},
new() { Key = new(Path.GetInvalidPathChars()), Data = ByteString.Empty},
new() { Key = string.Empty, Data = ByteString.Empty},
new() { Key = " ", Data = ByteString.Empty},
new() { Key = "\t", Data = ByteString.Empty},
new() { Key = "\n", Data = ByteString.Empty},
new() { Key = "\r", Data = ByteString.Empty}
};

sut.ProcessPullRequest(cache);

sut.UnchangedFilesPath.Should().BeNull();
Expand Down Expand Up @@ -303,7 +300,8 @@ private sealed class CacheContext : IDisposable
public readonly string Root;
public readonly List<string> Paths = new();
public readonly CacheProcessor Sut;
public readonly AnalysisCacheMsg Cache = new();

private readonly IList<SensorCacheEntry> cache = new List<SensorCacheEntry>();

public CacheContext(CacheProcessorTests owner, string commandLineArgs = "/k:key")
{
Expand All @@ -320,19 +318,23 @@ public CacheContext(CacheProcessorTests owner, string commandLineArgs = "/k:key"
Root = TestUtils.CreateTestSpecificFolderWithSubPaths(owner.TestContext);
var factory = new MockObjectFactory(owner.logger);
var settings = Mock.Of<IBuildSettings>(x => x.SourcesDirectory == Root && x.SonarConfigDirectory == Root);
factory.Server.Cache = Cache;
factory.Server.Cache = cache;
Sut = new CacheProcessor(factory.Server, CreateProcessedArgs(factory.Logger, commandLineArgs), settings, factory.Logger);
foreach (var (relativeFilePath, content) in fileData)
{
var fullFilePath = Path.GetFullPath(CreateFile(Root, relativeFilePath, content, Encoding.UTF8));
Paths.Add(fullFilePath);
Cache.Map.Add(relativeFilePath, ByteString.CopyFrom(Sut.ContentHash(fullFilePath)));
cache.Add(new SensorCacheEntry
{
Key = relativeFilePath,
Data = ByteString.CopyFrom(Sut.ContentHash(fullFilePath))
});
}
Sut.PullRequestCacheBasePath.Should().Be(Root, "Cache files must exist on expected path.");
}

public void ProcessPullRequest() =>
Sut.ProcessPullRequest(Cache);
Sut.ProcessPullRequest(cache);

public void Dispose() =>
Sut.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using FluentAssertions;
using SonarScanner.MSBuild.Common;
using SonarScanner.MSBuild.PreProcessor.Protobuf;
using SonarScanner.MSBuild.PreProcessor.Roslyn.Model;

Expand All @@ -37,7 +36,7 @@ internal class MockSonarWebService : ISonarWebService
private readonly List<string> warnings = new();

public ServerDataModel Data { get; } = new();
public AnalysisCacheMsg Cache { get; set; }
public IList<SensorCacheEntry> Cache { get; set; }
public Func<Task<bool>> IsServerLicenseValidImplementation { get; set; } = () => Task.FromResult(true);
public Action TryGetQualityProfilePreprocessing { get; set; } = () => { };

Expand Down Expand Up @@ -129,7 +128,7 @@ Task<bool> ISonarWebService.TryDownloadEmbeddedFile(string pluginKey, string emb
}
}

public Task<AnalysisCacheMsg> DownloadCache(string projectKey, string branch) =>
public Task<IList<SensorCacheEntry>> DownloadCache(string projectKey, string branch) =>
Task.FromResult(projectKey == "key-no-cache" ? null : Cache);

Task<Version> ISonarWebService.GetServerVersion()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -986,35 +986,48 @@ public async Task DownloadCache_NullArguments()
[TestMethod]
public async Task DownloadCache_DeserializesMessage()
{
using var stream = CreateCacheStream(new AnalysisCacheMsg { Map = { { "key", ByteString.CopyFromUtf8("value") } } });
using var stream = CreateCacheStream(new SensorCacheEntry { Key = "key", Data = ByteString.CopyFromUtf8("value") });
var sut = new SonarWebService(MockIDownloader(stream), ServerUrl, logger);

var result = await sut.DownloadCache(ProjectKey, ProjectBranch);

result.Map.Count.Should().Be(1);
result.Map["key"].ToStringUtf8().Should().Be("value");
result.Should().ContainSingle();
result.Single(x => x.Key == "key").Data.ToStringUtf8().Should().Be("value");
logger.AssertDebugLogged("Downloading cache. Project key: project-key, branch: project-branch.");
}

[TestMethod]
public async Task DownloadCache_WhenDownloadStreamReturnsNull_ReturnsNull()
public async Task DownloadCache_WhenCacheStreamReadThrows_ReturnsEmptyCollection()
{
var streamMock = new Mock<Stream>();
streamMock.Setup(x => x.Length).Throws<InvalidOperationException>();
var sut = new SonarWebService(MockIDownloader(streamMock.Object), ServerUrl, logger);

var result = await sut.DownloadCache(ProjectKey, ProjectBranch);

result.Should().BeEmpty();
logger.AssertDebugLogged("Incremental PR analysis: an error occurred while deserializing the cache entries! Operation is not valid due to the current state of the object.");
}

[TestMethod]
public async Task DownloadCache_WhenDownloadStreamReturnsNull_ReturnsEmptyCollection()
{
var sut = new SonarWebService(MockIDownloader(null), ServerUrl, logger);

var result = await sut.DownloadCache(ProjectKey, ProjectBranch);

result.Should().BeNull();
result.Should().BeEmpty();
}

[TestMethod]
public async Task DownloadCache_WhenDownloadStreamThrows_ReturnsNull()
public async Task DownloadCache_WhenDownloadStreamThrows_ReturnsEmptyCollection()
{
var downloaderMock = Mock.Of<IDownloader>(x => x.DownloadStream(It.IsAny<Uri>()) == Task.FromException<Stream>(new HttpRequestException()));
var sut = new SonarWebService(downloaderMock, ServerUrl, logger);

var result = await sut.DownloadCache(ProjectKey, ProjectBranch);

result.Should().BeNull();
result.Should().BeEmpty();
}

[DataTestMethod]
Expand Down Expand Up @@ -1135,7 +1148,7 @@ public async Task DownloadCache_RequestUrl(string hostUrl, string downloadUrl)

var result = await sut.DownloadCache(ProjectKey, ProjectBranch);

result.Map.Should().BeEmpty();
result.Should().BeEmpty();
}

[TestMethod]
Expand Down Expand Up @@ -1169,7 +1182,7 @@ public async Task GetProperties_RequestUrl(string hostUrl, string versionUrl, st
private static Stream CreateCacheStream(IMessage message)
{
var stream = new MemoryStream();
message.WriteTo(stream);
message.WriteDelimitedTo(stream);
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -954,22 +954,16 @@ public void incrementalPrAnalysis_NoCache() throws IOException {

@Test
public void incrementalPrAnalysis_ProducesUnchangedFiles() throws IOException {
Assume.assumeTrue(ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(9, 4)); // Cache API was introduced in 9.4
Assume.assumeTrue(ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(9, 9)); // Public cache API was introduced in 9.9

String projectKey = "incremental-pr-analysis";
String projectKey = "IncrementalPRAnalysis";
String baseBranch = TestUtils.getDefaultBranchName(ORCHESTRATOR);
Path projectDir = TestUtils.projectDir(temp, "IncrementalPRAnalysis");
// This is a temporary solution for uploading a valid cache.
// The file was generated from https://github.com/SonarSource/sonar-dotnet/commit/28c1224aca62968fb52b0332d6f6b7ef3da11f2b commit.
// In order to regenerate this file:
// - Update `FileStatusCacheSensor` to trim the paths correctly and save only the relative paths.
// - build the plugin
// - add it to your SonarQube
// - start SonarQube and analyze `IncrementalPRAnalysis` project with `/d:sonar.scanner.keepReport=true`
// - archive the content of `.sonarqube\out\.sonar\scanner-report\` (only the content, without parent folder) as `scanner-report.zip`
File reportZip = projectDir.resolve("scanner-report.zip").toFile();

uploadAnalysisWithCache(projectKey, baseBranch, reportZip);

BuildResult firstAnalysisResult = runAnalysisWithoutProjectBasedDir(projectDir);
assertTrue(firstAnalysisResult.isSuccess());

waitForCacheInitialization(projectKey, baseBranch);

File fileToBeChanged = projectDir.resolve("IncrementalPRAnalysis\\WithChanges.cs").toFile();
BufferedWriter writer = new BufferedWriter(new FileWriter(fileToBeChanged, true));
Expand All @@ -986,7 +980,7 @@ public void incrementalPrAnalysis_ProducesUnchangedFiles() throws IOException {
assertTrue(result.isSuccess());
assertThat(result.getLogs()).contains("Processing analysis cache");
assertThat(result.getLogs()).contains("Processing pull request with base branch '" + baseBranch + "'.");
assertThat(result.getLogs()).contains("Downloading cache. Project key: incremental-pr-analysis, branch: " + baseBranch + ".");
assertThat(result.getLogs()).contains("Downloading cache. Project key: IncrementalPRAnalysis, branch: " + baseBranch + ".");

Path buildDirectory = VstsUtils.isRunningUnderVsts() ? Path.of(VstsUtils.getEnvBuildDirectory()) : projectDir;
Path expectedUnchangedFiles = buildDirectory.resolve(".sonarqube\\conf\\UnchangedFiles.txt");
Expand All @@ -1000,22 +994,7 @@ public void incrementalPrAnalysis_ProducesUnchangedFiles() throws IOException {
.doesNotContain("WithChanges.cs"); // Was modified
}

private void uploadAnalysisWithCache(String projectKey, String baseBranch, File reportZip) throws IOException {
// ToDo: Once the second part of incremental PR analysis is implemented in sonar-dotnet,
// replace this with an actual cache upload by the plugin and delete the maven dependency `awaitility`.
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost uploadFile = new HttpPost(ORCHESTRATOR.getServer().getUrl() + "/api/ce/submit");
String tokenPassword = ORCHESTRATOR.getDefaultAdminToken() + ":"; // Empty password
uploadFile.addHeader("Authorization", "Basic "+ Base64.getEncoder().encodeToString(tokenPassword.getBytes(StandardCharsets.UTF_8)));

MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.addTextBody("projectKey", projectKey);
builder.addBinaryBody("report", new FileInputStream(reportZip), ContentType.APPLICATION_OCTET_STREAM, reportZip.getName());

HttpEntity multipart = builder.build();
uploadFile.setEntity(multipart);
httpClient.execute(uploadFile);

private void waitForCacheInitialization(String projectKey, String baseBranch) {
await()
.pollInterval(Duration.ofSeconds(1))
.atMost(Duration.ofSeconds(120))
Expand Down
12 changes: 7 additions & 5 deletions src/SonarScanner.MSBuild.PreProcessor/CacheProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
Expand Down Expand Up @@ -67,7 +68,7 @@ public async Task Execute()
else
{
logger.LogInfo(Resources.MSG_Processing_PullRequest_Branch, baseBranch);
if (await server.DownloadCache(localSettings.ProjectKey, baseBranch) is { } cache)
if (await server.DownloadCache(localSettings.ProjectKey, baseBranch) is { Count: > 0 } cache)
{
ProcessPullRequest(cache);
}
Expand All @@ -89,12 +90,13 @@ public async Task Execute()
return sha256.ComputeHash(stream);
}

internal /* for testing */ void ProcessPullRequest(AnalysisCacheMsg cache)
internal /* for testing */ void ProcessPullRequest(IList<SensorCacheEntry> cache)
{
var invalidPathChars = Path.GetInvalidPathChars();
var unchangedFiles = cache.Map

var unchangedFiles = cache
.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.IndexOfAny(invalidPathChars) < 0)
.Select(x => new { Hash = x.Value, Path = Path.Combine(PullRequestCacheBasePath, x.Key) })
.Select(x => new { Hash = x.Data, Path = Path.Combine(PullRequestCacheBasePath, x.Key) })
.Where(x => File.Exists(x.Path) && ContentHash(x.Path).SequenceEqual(x.Hash))
.Select(x => Path.GetFullPath(x.Path))
.ToArray();
Expand All @@ -103,7 +105,7 @@ public async Task Execute()
UnchangedFilesPath = Path.Combine(buildSettings.SonarConfigDirectory, "UnchangedFiles.txt");
File.WriteAllLines(UnchangedFilesPath, unchangedFiles);
}
logger.LogInfo(Resources.MSG_UnchangedFilesStats, unchangedFiles.Length, cache.Map.Count);
logger.LogInfo(Resources.MSG_UnchangedFilesStats, unchangedFiles.Length, cache.Count);
}

private static string PullRequestBaseBranch(ProcessedArgs localSettings) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public interface ISonarWebService : IDisposable
/// <param name="targetDirectory">The directory to which the file should be downloaded</param>
Task<bool> TryDownloadEmbeddedFile(string pluginKey, string embeddedFileName, string targetDirectory);

Task<AnalysisCacheMsg> DownloadCache(string projectKey, string branch);
Task<IList<SensorCacheEntry>> DownloadCache(string projectKey, string branch);

Task<Version> GetServerVersion();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@

syntax = "proto3";

// Source https://github.com/SonarSource/sonar-enterprise/blob/master/sonar-scanner-protocol/src/main/protobuf/scanner_internal.proto
option csharp_namespace = "SonarScanner.MSBuild.PreProcessor.Protobuf";
option optimize_for = SPEED;

message AnalysisCacheMsg {
map<string, bytes> map = 1;

message SensorCacheEntry {
string key = 1;
bytes data = 2;
}
10 changes: 9 additions & 1 deletion src/SonarScanner.MSBuild.PreProcessor/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/SonarScanner.MSBuild.PreProcessor/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ Use '/?' or '/h' to see the help message.</value>
<data name="WARN_NoPullRequestCacheBasePath" xml:space="preserve">
<value>Cannot determine project base path. Incremental PR analysis is disabled.</value>
</data>
<data name="MSG_IncrementalPRCacheEntryDeserialization" xml:space="preserve">
<value>Incremental PR analysis: an error occurred while deserializing the cache entries! {0}</value>
</data>
<data name="MSG_NoCacheData" xml:space="preserve">
<value>Cache data is not available. Incremental PR analysis is disabled.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<ProjectReference Include="..\SonarScanner.MSBuild.Common\SonarScanner.MSBuild.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.21.9" />
<PackageReference Include="Grpc.Tools" Version="2.50.0">
<PackageReference Include="Google.Protobuf" Version="3.21.12" />
<PackageReference Include="Grpc.Tools" Version="2.51.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
27 changes: 25 additions & 2 deletions src/SonarScanner.MSBuild.PreProcessor/SonarWebService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,37 @@ public async Task<bool> TryDownloadEmbeddedFile(string pluginKey, string embedde
}, uri);
}

public Task<AnalysisCacheMsg> DownloadCache(string projectKey, string branch)
public Task<IList<SensorCacheEntry>> DownloadCache(string projectKey, string branch)
{
_ = projectKey ?? throw new ArgumentNullException(nameof(projectKey));
_ = branch ?? throw new ArgumentNullException(nameof(branch));

logger.LogDebug(Resources.MSG_DownloadingCache, projectKey, branch);
var uri = GetUri("api/analysis_cache/get?project={0}&branch={1}", projectKey, branch);
return downloader.DownloadStream(uri).ContinueWith(x => x.IsFaulted || x.Result == null ? null : AnalysisCacheMsg.Parser.ParseFrom(x.Result));
return downloader.DownloadStream(uri).ContinueWith(ParseCacheEntries);
}

private IList<SensorCacheEntry> ParseCacheEntries(Task<Stream> task)
{
if (task.IsFaulted || task.Result is not { } dataStream)
{
return new List<SensorCacheEntry>();
}

var cacheEntries = new List<SensorCacheEntry>();
try
{
while (dataStream.Position < dataStream.Length)
{
cacheEntries.Add(SensorCacheEntry.Parser.ParseDelimitedFrom(dataStream));
}
}
catch (Exception e)
{
logger.LogDebug(Resources.MSG_IncrementalPRCacheEntryDeserialization, e.Message);
}

return cacheEntries;
}

private async Task<bool> IsSonarCloud() =>
Expand Down

0 comments on commit ad740c7

Please sign in to comment.