From a42f9f371e6c6822169bfd6530ffb068911b4172 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:21:13 +0300 Subject: [PATCH] Launch artifacts extension (#148) * Simple artifacts collecting * Docs * Update Configuration.md * Update Configuration.md --- docs/Configuration.md | 15 +- .../LaunchArtifactsEventsObserver.cs | 76 ++++++++++ .../LaunchArtifacts/LaunchArtifactsTest.cs | 143 ++++++++++++++++++ .../ExtensionManagerFixture.cs | 2 +- 4 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 src/ReportPortal.Shared/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsEventsObserver.cs create mode 100644 test/ReportPortal.Shared.Tests/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsTest.cs diff --git a/docs/Configuration.md b/docs/Configuration.md index 7e1b2275..1fd06001 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -102,13 +102,24 @@ Requests are not repeated, only 1 attempt is allocated for all requests. # Reporting Experience If you want to redirect agent to send test results into some another way, there are several options: -- `Launch:Id` (UUID of existing launch) - agent will append test results into provided Launch ID. Launch should be *IN_PROGRESS* state, agent will not finish it. It's your responsibility to start and finish launch. Usefull for distributed test execution, where tests are running on different machines and you want to see consolidated report. +- `Launch:Id` (UUID of existing launch) - agent will append test results into provided Launch ID. Launch should be *IN_PROGRESS* state, agent will not finish it. It's your responsibility to start and finish launch. Useful for distributed test execution, where tests are running on different machines and you want to see consolidated report. - `Launch:Rerun` (true/false/yes/no) - agent will try to add new tests into existing launch (compared by name) or adds new attempt/retry for existing tests. - `Launch:RerunOf` (UUID of existing launch) - agent will try to add new tests into existing launch (by ID) or adds new attempt/retry for existing tests. Takes effect only if `Launch:Rerun` is `true`. +## Attachments +In additional of attaching artifacts during tests execution [dynamically](./Logging.md), it is possible to configure file attachments at launch level statically via files pattern. Set `Launch:Artifacts` configuration property to set of file patterns, and files will be automatically attached after tests execution. + +Example: +```json +{ + "launch": { + "artifacts": [ "*.log", "screenshots/*.png" ] + } +} +``` # Analytics -Each time when new launch is posted to RP server, reporting engine sends this fact to google analytics service. It doesn't collect sensetive information, just name and version of used engine/agent. +Each time when new launch is posted to RP server, reporting engine sends this fact to google analytics service. It doesn't collect sensitive information, just name and version of used engine/agent. This behavior can be turned off through `Analytics:Enabled` configuration property. \ No newline at end of file diff --git a/src/ReportPortal.Shared/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsEventsObserver.cs b/src/ReportPortal.Shared/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsEventsObserver.cs new file mode 100644 index 00000000..60206cfd --- /dev/null +++ b/src/ReportPortal.Shared/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsEventsObserver.cs @@ -0,0 +1,76 @@ +using ReportPortal.Client.Abstractions.Requests; +using ReportPortal.Shared.Extensibility.ReportEvents; +using ReportPortal.Shared.Internal.Logging; +using ReportPortal.Shared.MimeTypes; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace ReportPortal.Shared.Extensibility.Embedded.LaunchArtifacts +{ + public class LaunchArtifactsEventsObserver : IReportEventsObserver + { + private static readonly ITraceLogger _logger = TraceLogManager.Instance.GetLogger(typeof(LaunchArtifactsEventsObserver)); + + public string BaseDirectory { get; set; } = Environment.CurrentDirectory; + + public void Initialize(IReportEventsSource reportEventsSource) + { + reportEventsSource.OnBeforeLaunchFinishing += ReportEventsSource_OnBeforeLaunchFinishing; + } + + private void ReportEventsSource_OnBeforeLaunchFinishing(Reporter.ILaunchReporter launchReporter, ReportEvents.EventArgs.BeforeLaunchFinishingEventArgs args) + { + var artifactPaths = args.Configuration.GetValues("Launch:Artifacts", null); + + if (artifactPaths != null) + { + foreach (var filePattern in artifactPaths) + { + var artifacts = Directory.GetFiles(BaseDirectory, filePattern); + + foreach (var artifact in artifacts) + { + var createLogItemRequest = new CreateLogItemRequest + { + LaunchUuid = launchReporter.Info.Uuid, + Time = DateTime.UtcNow, + Level = Client.Abstractions.Models.LogLevel.Trace, + Text = Path.GetFileName(artifact), + }; + + AttachFile(artifact, ref createLogItemRequest); + + Task.Run(async () => await args.ClientService.LogItem.CreateAsync(createLogItemRequest)).GetAwaiter().GetResult(); + } + } + } + } + + private static void AttachFile(string filePath, ref CreateLogItemRequest request) + { + try + { + using (var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + using (var memoryStream = new MemoryStream()) + { + fileStream.CopyTo(memoryStream); + var bytes = memoryStream.ToArray(); + + request.Attach = new LogItemAttach + { + Name = Path.GetFileName(filePath), + MimeType = MimeTypeMap.GetMimeType(Path.GetExtension(filePath)), + Data = bytes + }; + } + } + } + catch (Exception ex) + { + request.Text = $"{request.Text}\n> Couldn't read content of `{filePath}` file. \n{ex}"; + } + } + } +} diff --git a/test/ReportPortal.Shared.Tests/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsTest.cs b/test/ReportPortal.Shared.Tests/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsTest.cs new file mode 100644 index 00000000..0fb5f91c --- /dev/null +++ b/test/ReportPortal.Shared.Tests/Extensibility/Embedded/LaunchArtifacts/LaunchArtifactsTest.cs @@ -0,0 +1,143 @@ +using FluentAssertions; +using Moq; +using ReportPortal.Client.Abstractions.Requests; +using ReportPortal.Shared.Configuration; +using ReportPortal.Shared.Extensibility; +using ReportPortal.Shared.Extensibility.Embedded.LaunchArtifacts; +using ReportPortal.Shared.Tests.Helpers; +using System.IO; +using System.Threading; +using Xunit; + +namespace ReportPortal.Shared.Tests.Extensibility.Embedded.LaunchArtifacts +{ + public class LaunchArtifactsTest + { + private readonly IExtensionManager _extensionManager; + + public LaunchArtifactsTest() + { + _extensionManager = new Shared.Extensibility.ExtensionManager(); + _extensionManager.ReportEventObservers.Add(new LaunchArtifactsEventsObserver()); + } + + [Fact] + public void ShouldNotAttachArtifacts() + { + var client = new MockServiceBuilder().Build(); + + var launchReporter = new LaunchReporterBuilder(client.Object).With(_extensionManager).Build(1, 0, 0); + launchReporter.Sync(); + + client.Verify(s => s.LogItem.CreateAsync(It.IsAny(), default), Times.Never()); + } + + [Fact] + public void ShouldAttachSingleArtifactByPath() + { + File.WriteAllBytes("test_file_1.txt", new byte[] { 1, 2, 3 }); + + CreateLogItemRequest request = null; + + var client = new MockServiceBuilder().Build(); + client.Setup(s => s.LogItem.CreateAsync(It.IsAny(), default)) + .Callback((a, b) => request = a); + + var config = new ConfigurationBuilder().Build(); + config.Properties["launch:artifacts"] = "test_file_1.txt"; + + var launchReporter = new LaunchReporterBuilder(client.Object).With(_extensionManager).WithConfiguration(config).Build(1, 0, 0); + launchReporter.Sync(); + + client.Verify(s => s.LogItem.CreateAsync(It.IsAny(), default), Times.Once); + + request.Should().NotBeNull(); + request.Text.Should().Be("test_file_1.txt"); + + request.Attach.Should().NotBeNull(); + request.Attach.Name.Should().Be("test_file_1.txt"); + request.Attach.MimeType.Should().Be("text/plain"); + request.Attach.Data.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + } + + [Fact] + public void ShouldAttachSeveralArtifactsByPath() + { + File.Create("test_file_1.txt").Close(); + File.Create("test_file_2.txt").Close(); + + var client = new MockServiceBuilder().Build(); + + var config = new ConfigurationBuilder().Build(); + config.Properties["launch:artifacts"] = "test_file_1.txt;test_file_2.txt"; + + var launchReporter = new LaunchReporterBuilder(client.Object).With(_extensionManager).WithConfiguration(config).Build(1, 0, 0); + launchReporter.Sync(); + + client.Verify(s => s.LogItem.CreateAsync(It.IsAny(), default), Times.Exactly(2)); + } + + [Fact] + public void ShouldAttachSingleArtifactByPattern() + { + File.WriteAllBytes("test_file_1.txt", new byte[] { 1, 2, 3 }); + + CreateLogItemRequest request = null; + + var client = new MockServiceBuilder().Build(); + client.Setup(s => s.LogItem.CreateAsync(It.IsAny(), default)) + .Callback((a, b) => request = a); + + var config = new ConfigurationBuilder().Build(); + config.Properties["launch:artifacts"] = "test_*_1.txt"; + + var launchReporter = new LaunchReporterBuilder(client.Object).With(_extensionManager).WithConfiguration(config).Build(1, 0, 0); + launchReporter.Sync(); + + client.Verify(s => s.LogItem.CreateAsync(It.IsAny(), default), Times.Once); + } + + [Fact] + public void ShouldAttachSingleArtifactByDir() + { + Directory.CreateDirectory("a/b/c"); + File.WriteAllBytes("a/b/c/abc.txt", new byte[] { 1, 2, 3 }); + + CreateLogItemRequest request = null; + + var client = new MockServiceBuilder().Build(); + client.Setup(s => s.LogItem.CreateAsync(It.IsAny(), default)) + .Callback((a, b) => request = a); + + var config = new ConfigurationBuilder().Build(); + config.Properties["launch:artifacts"] = "a/b/c/*.txt"; + + var launchReporter = new LaunchReporterBuilder(client.Object).With(_extensionManager).WithConfiguration(config).Build(1, 0, 0); + launchReporter.Sync(); + + client.Verify(s => s.LogItem.CreateAsync(It.IsAny(), default), Times.Once); + } + + [Fact] + public void ShouldAttachMessageWithException() + { + File.Create("test_file_open.txt"); // leaves it open + + CreateLogItemRequest request = null; + + var client = new MockServiceBuilder().Build(); + client.Setup(s => s.LogItem.CreateAsync(It.IsAny(), default)) + .Callback((a, b) => request = a); + + var config = new ConfigurationBuilder().Build(); + config.Properties["launch:artifacts"] = "test_file_open.txt"; + + var launchReporter = new LaunchReporterBuilder(client.Object).With(_extensionManager).WithConfiguration(config).Build(1, 0, 0); + launchReporter.Sync(); + + client.Verify(s => s.LogItem.CreateAsync(It.IsAny(), default), Times.Once); + + request.Text.Should().Contain("Couldn't read"); + } + } +} diff --git a/test/ReportPortal.Shared.Tests/Extensibility/ExtensionManager/ExtensionManagerFixture.cs b/test/ReportPortal.Shared.Tests/Extensibility/ExtensionManager/ExtensionManagerFixture.cs index 8c80fcc7..8fc7d324 100644 --- a/test/ReportPortal.Shared.Tests/Extensibility/ExtensionManager/ExtensionManagerFixture.cs +++ b/test/ReportPortal.Shared.Tests/Extensibility/ExtensionManager/ExtensionManagerFixture.cs @@ -15,7 +15,7 @@ public void ShouldExploreExtensions() manager.Explore(Environment.CurrentDirectory); manager.ReportEventObservers.Count.Should() - .Be(2, "default and google analytic observers should be registered by default"); + .Be(3, "normalizer, google analytic, launch artifacts - observers should be registered by default"); manager.CommandsListeners.Should().HaveCount(1); }