private async Task ProcessArtifactAsync(BuildSyncContext context, BuildArtifact artifact, CancellationToken stoppingToken) { // Stream the artifact down to disk var artifactFile = await SaveArtifactAsync(artifact, stoppingToken); try { using var archive = ZipFile.OpenRead(artifactFile); foreach (var file in archive.Entries) { using (_logger.BeginScope("File: {ArtifactFile}", file.FullName)) { if (file.Name.EndsWith(".xml")) { _logger.LogTrace(new EventId(0, "ProcessingFile"), "Processing file {ArtifactFile}...", file); await ProcessTestResultFileAsync(context, file, stoppingToken); _logger.LogTrace(new EventId(0, "ProcessedFile"), "Processed file {ArtifactFile}.", file); } else { _logger.LogTrace(new EventId(0, "SkippingFile"), "Skipping artifact file {ArtifactFile}.", file); } } } } catch (OperationCanceledException) { _logger.LogDebug(new EventId(0, "CancellingArtifactProcessing"), "Cancelling processing of artifact: {ArtifactName}.", artifact.Name); throw; } catch (Exception ex) { _logger.LogError(new EventId(0, "ErrorProcessingArtifact"), ex, "Error processing artifact: {ArtifactName}.", artifact.Name); // We don't want to continue processing this build if an artifact failed. That way we can try again later. throw; } finally { try { _logger.LogTrace(new EventId(0, "DeletingTemporaryFile"), "Deleting temporary file {TempFile} for Artifact {ArtifactName}.", artifactFile, artifact.Name); File.Delete(artifactFile); } catch (Exception ex) { _logger.LogWarning(new EventId(0, "ErrorDeletingTemporaryFile"), ex, "Error deleting temporary file: {TempFile}.", artifactFile); } } }
private async Task ProcessBuildAsync(BuildSyncContext context, CancellationToken stoppingToken) { // Mark this build as in progress. context.DbBuild.SyncStatus = SyncStatus.InProgress; context.DbBuild.SyncStartedUtc = DateTime.UtcNow; await context.Db.SaveChangesAsync(); _logger.LogDebug(new EventId(0, "StartedSyncForBuild"), "Started Sync for Build #{BuildNumber}", context.DbBuild.BuildNumber); try { var artifacts = await context.GetArtifactsAsync(cancellationToken : stoppingToken); foreach (var artifact in artifacts) { _logger.LogDebug(new EventId(0, "ProcessingArtifact"), "Processing artifact {ArtifactName}...", artifact.Name); await ProcessArtifactAsync(context, artifact, stoppingToken); _logger.LogDebug(new EventId(0, "ProcessedArtifact"), "Processed artifact {ArtifactName}.", artifact.Name); } context.DbBuild.SyncStatus = SyncStatus.Complete; context.DbBuild.SyncCompleteUtc = DateTime.UtcNow; await context.Db.SaveChangesAsync(); _logger.LogDebug(new EventId(0, "SavedBuild"), "Saved all results from build #{BuildNumber}", context.DbBuild.BuildNumber); } catch (OperationCanceledException) { _logger.LogDebug(new EventId(0, "CancellingBuildProcessing"), "Cancelling processing of build: #{BuildNumber}.", context.DbBuild.BuildNumber); context.DbBuild.SyncStatus = SyncStatus.Cancelled; await context.Db.SaveChangesAsync(); throw; } catch (Exception ex) { context.DbBuild.SyncStatus = SyncStatus.Failed; await context.Db.SaveChangesAsync(); _logger.LogError(new EventId(0, "ErrorProcessingBuild"), ex, "Error Processing Build #{BuildNumber}.", context.DbBuild.BuildNumber); } }
private async Task ProcessBranchAsync(PipelineScrapeContext context, PipelineConfig config, string branch, CancellationToken stoppingToken) { // Look up the most recent build (for now) _logger.LogDebug(new EventId(0, "FetchingBuilds"), "Fetching builds for {PipelineProject}/{PipelineName} in {BranchName}...", config.Project, config.Name, branch); var builds = await context.GetBuildsAsync(branch, stoppingToken); _logger.LogInformation(new EventId(0, "FetchedBuilds"), "Fetched {BuildCount} builds.", builds.Count); foreach (var build in builds) { using (_logger.BeginScope("Build {BuildId} #{BuildNumber}", build.Id, build.BuildNumber, build.SourceVersion)) { using (var scope = _scopeFactory.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService <TestResultsDbContext>(); // Load the Build from the new db context var dbBuild = await db.Builds.FirstOrDefaultAsync(b => b.Id == build.Id); if (dbBuild == null) { _logger.LogError(new EventId(0, "CouldNotFindBuild"), "Could not find build #{BuildNumber} (ID: {BuildDbId}) in the database!", build.BuildNumber, build.Id); continue; } var buildContext = new BuildSyncContext(context, dbBuild, db); // TODO: Retry of unsynced builds? _logger.LogInformation(new EventId(0, "ProcessingBuild"), "Processing build #{BuildNumber}...", build.BuildNumber); var sw = Stopwatch.StartNew(); await ProcessBuildAsync(buildContext, stoppingToken); _logger.LogInformation(new EventId(0, "ProcessedBuild"), "Processed build #{BuildNumber} in {Elapsed}.", build.BuildNumber, sw.Elapsed); } } } }
private async Task ProcessTestResultFileAsync(BuildSyncContext context, ZipArchiveEntry file, CancellationToken stoppingToken) { try { using var stream = file.Open(); var doc = await XDocument.LoadAsync(stream, LoadOptions.None, stoppingToken); // Remove the extension but keep the full relative path to get the run name. var runName = file.FullName.Substring(0, file.FullName.Length - 4); var dbRunId = await context.PipelineContext.ResultsContext.GetOrCreateRunAsync(runName, stoppingToken); _logger.LogTrace(new EventId(0, "ParsingTestResults"), "Parsing test results."); var assemblies = XUnitTestResultsFormat.Parse(doc); _logger.LogTrace(new EventId(0, "ProcessingTestResults"), "Processing test results..."); foreach (var assembly in assemblies) { foreach (var collection in assembly.Collections) { var dbCollection = dbAssembly.Collections.FirstOrDefault(c => c.Name == collection.Name); if (dbCollection == null) { dbCollection = new PipelineTestAssembly() { Name = assembly.Name }; context.Db.TestAssemblies.Add(dbAssembly); } foreach (var method in collection.Methods) { var dbMethodId = await context.PipelineContext.ResultsContext.GetOrCreateMethodAsync(dbCollectionId, method.Type, method.Name, stoppingToken); foreach (var result in method.Results) { // Truncate off the type name var resultName = result.Name.StartsWith($"{method.Type}.") ? result.Name.Substring(method.Type.Length + 1) : result.Name; var dbCaseId = await context.PipelineContext.ResultsContext.GetOrCreateTestCaseAsync(dbMethodId, resultName, stoppingToken); var quarantinedOn = new List <string>(); foreach (var trait in result.Traits) { if (trait.Name.StartsWith("Flaky:")) { quarantinedOn.Add(trait.Name.Substring(6)); } else if (trait.Name.StartsWith("Quarantined:")) { quarantinedOn.Add(trait.Name.Substring(12)); } } var dbResult = new PipelineTestResult() { BuildId = context.DbBuild.Id, CaseId = dbCaseId, RunId = dbRunId, Traits = string.Join(";", result.Traits.Select(t => $"{t.Name}={t.Value}")), Quarantined = result.Traits.Any(t => t.Name.StartsWith("Flaky:") || t.Name.StartsWith("Quarantined:")), QuarantinedOn = string.Join(";", result.Traits.Where(t => t.Name.StartsWith("Flaky:")).Select(t => t.Name.Substring(6))), Result = result.Outcome switch { FailureTestOutcome f => TestResultKind.Fail, SkippedTestOutcome s => TestResultKind.Skip, SuccessfulTestOutcome _ => TestResultKind.Pass, _ => TestResultKind.Unknown, } }; context.Db.TestResults.Add(dbResult); } } } } // Save results per-file. _logger.LogTrace(new EventId(0, "SavingTestResults"), "Saving test results to database..."); await context.Db.SaveChangesAsync(); _logger.LogTrace(new EventId(0, "SavedTestResults"), "Saved test results to database"); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError(new EventId(0, "ErrorProcessingTestResults"), ex, "Error processing test result file: {TestResultsFile}", file.FullName); throw; } }