< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 134
Coverable lines: 134
Total lines: 222
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 38
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/dotnet-snap/dotnet-snap/src/Dotnet.Installer.Core/Services/Implementations/ManifestService.cs

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.RegularExpressions;
 3using Dotnet.Installer.Core.Models;
 4using Dotnet.Installer.Core.Services.Contracts;
 5
 6namespace Dotnet.Installer.Core.Services.Implementations;
 7
 8public partial class ManifestService : IManifestService
 9{
 010    private static readonly Regex DotnetVersionPattern = new (
 011        pattern: @"\A(?'major'\d+)(?:\.(?'minor'\d+))?\z");
 12
 013    private static readonly JsonSerializerOptions JsonSerializerOptions = new()
 014    {
 015        PropertyNameCaseInsensitive = true
 016    };
 17
 018    private List<Component> _local = [];
 019    private List<Component> _remote = [];
 020    private List<Component> _merged = [];
 021    private bool _includeUnsupported = false;
 22
 023    public string SnapConfigurationLocation => SnapConfigPath;
 24    public string DotnetInstallLocation =>
 025        Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR")
 026            ?? throw new ApplicationException("DOTNET_INSTALL_DIR is not set.");
 27
 28    /// <summary>
 29    /// The local manifest, which includes currently installed components.
 30    /// </summary>
 31    public IEnumerable<Component> Local
 32    {
 033        get => _local;
 034        private set => _local = value.ToList();
 35    }
 36
 37    /// <summary>
 38    /// The remote manifest, which includes available components to be downloaded.
 39    /// </summary>
 40    public IEnumerable<Component> Remote
 41    {
 042        get => _remote;
 043        private set => _remote = value.ToList();
 44    }
 45
 46    /// <summary>
 47    /// The merged manifest, which is the local and remote manifests merged into one list.
 48    /// Installed components can be told apart by verifying whether <c>Installation != null</c>.
 49    /// </summary>
 50    public IEnumerable<Component> Merged
 51    {
 052        get => _merged;
 053        private set => _merged = value.ToList();
 54    }
 55
 56    public Task Initialize(bool includeUnsupported = false, CancellationToken cancellationToken = default)
 057    {
 058        _includeUnsupported = includeUnsupported;
 059        return Refresh(cancellationToken);
 060    }
 61
 62    public async Task Add(Component component, CancellationToken cancellationToken = default)
 063    {
 064        component.Installation = new Installation
 065        {
 066            InstalledAt = DateTimeOffset.UtcNow
 067        };
 068        _local.Add(component);
 069        await Save(cancellationToken);
 070        await Refresh(cancellationToken);
 071    }
 72
 73    public async Task Remove(Component component, CancellationToken cancellationToken = default)
 074    {
 075        var componentToRemove = _local.FirstOrDefault(c => c.Key == component.Key);
 076        if (componentToRemove is not null)
 077        {
 078            _local.Remove(componentToRemove);
 079        }
 080        await Save(cancellationToken);
 081        await Refresh(cancellationToken);
 082    }
 83
 84    public Component? MatchRemoteComponent(string component, string version)
 085    {
 086        return MatchComponent(component, version, remote: true);
 087    }
 88
 89    public Component? MatchLocalComponent(string component, string version)
 090    {
 091        return MatchComponent(component, version, remote: false);
 092    }
 93
 94    private Component? MatchComponent(string component, string version, bool remote = true)
 095    {
 096        if (string.IsNullOrWhiteSpace(component)) return null;
 097        if (string.IsNullOrWhiteSpace(version)) return null;
 98
 099        var components = remote ? _remote : _local;
 100
 0101        if (version.Equals("lts", StringComparison.CurrentCultureIgnoreCase))
 0102        {
 0103            return components
 0104                .Where(c => c.IsLts && c.Name.Equals(component, StringComparison.CurrentCultureIgnoreCase))
 0105                .MaxBy(c => c.MajorVersion);
 106        }
 107
 0108        if (version.Equals("latest", StringComparison.CurrentCultureIgnoreCase))
 0109        {
 0110            return components
 0111                .Where(c => c.Name.Equals(component, StringComparison.CurrentCultureIgnoreCase))
 0112                .MaxBy(c => c.MajorVersion);
 113        }
 114
 0115        var parsedVersion = DotnetVersionPattern.Match(version);
 116
 0117        if (!parsedVersion.Success) return null;
 0118        if (parsedVersion.Groups["minor"].Success
 0119            && int.Parse(parsedVersion.Groups["minor"].Value) != 0)
 0120        {
 0121            return null;
 122        }
 123
 0124        var majorVersion = int.Parse(parsedVersion.Groups["major"].Value);
 125
 0126        return components.Where(c =>
 0127                c.MajorVersion == majorVersion &&
 0128                c.Name.Equals(component, StringComparison.CurrentCultureIgnoreCase))
 0129            .MaxBy(c => c.MajorVersion);
 0130    }
 131}

/home/runner/work/dotnet-snap/dotnet-snap/src/Dotnet.Installer.Core/Services/Implementations/ManifestService.Private.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.Json;
 3using Dotnet.Installer.Core.Models;
 4
 5namespace Dotnet.Installer.Core.Services.Implementations;
 6
 7public partial class ManifestService
 8{
 09    private static readonly string SnapConfigPath = Path.Join(
 010        Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR"), "..", "snap");
 11
 012    private static readonly string LocalManifestPath = Path.Join(SnapConfigPath, "manifest.json");
 13
 14    private static async Task<List<Component>> LoadLocal(CancellationToken cancellationToken = default)
 015    {
 016        if (!File.Exists(LocalManifestPath)) return [];
 17
 018        await using var fs = File.OpenRead(LocalManifestPath);
 019        var result = await JsonSerializer.DeserializeAsync<List<Component>>(
 020            fs, JsonSerializerOptions, cancellationToken
 021        );
 22
 023        return result ?? [];
 024    }
 25
 26    private static async Task<List<Component>> LoadRemote(bool includeUnsupported = false,
 27        CancellationToken cancellationToken = default)
 028    {
 029        var filesToRead = new List<string>
 030        {
 031            Path.Join("/", "snap", "dotnet-manifest", "current", "supported.json")
 032        };
 33
 034        if (includeUnsupported)
 035        {
 036            filesToRead.Add(Path.Join("/", "snap", "dotnet-manifest", "current", "unsupported.json"));
 037        }
 38
 039        var components = new List<Component>();
 040        foreach (var contentStream in filesToRead.Select(File.OpenRead))
 041        {
 042            var currentComponents = await JsonSerializer.DeserializeAsync<List<Component>>(
 043                contentStream,
 044                JsonSerializerOptions,
 045                cancellationToken: cancellationToken);
 46
 047            if (currentComponents is not null)
 048            {
 049                components.AddRange(currentComponents);
 050            }
 051        }
 52
 053        return components;
 054    }
 55
 56    private async Task Refresh(CancellationToken cancellationToken = default)
 057    {
 058        _local = await LoadLocal(cancellationToken);
 059        _remote = await LoadRemote(_includeUnsupported, cancellationToken);
 060        _merged = Merge(_remote, _local);
 061    }
 62
 63    private static List<Component> Merge(List<Component> remoteComponents, List<Component> localComponents)
 064    {
 065        var result = new List<Component>();
 066        result.AddRange(remoteComponents);
 67
 068        foreach (var localComponent in localComponents)
 069        {
 070            if (result.All(c => c.Key != localComponent.Key))
 071            {
 072                result.Add(localComponent);
 073            }
 74            else
 075            {
 076                var remote = result.First(c => c.Key == localComponent.Key);
 077                remote.Installation = localComponent.Installation;
 078            }
 079        }
 80
 081        return result;
 082    }
 83
 84    private async Task Save(CancellationToken cancellationToken = default)
 085    {
 086        await using var sw = new StreamWriter(LocalManifestPath, append: false, Encoding.UTF8);
 087        var content = JsonSerializer.Serialize(_local, JsonSerializerOptions);
 088        var stringBuilder = new StringBuilder(content);
 089        await sw.WriteLineAsync(stringBuilder, cancellationToken);
 090    }
 91}