< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 136
Coverable lines: 136
Total lines: 230
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 44
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;
 022    private bool _includePrerelease = false;
 23
 024    public string SnapConfigurationLocation => SnapConfigPath;
 25    public string DotnetInstallLocation =>
 026        Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR")
 027            ?? throw new ApplicationException("DOTNET_INSTALL_DIR is not set.");
 28
 29    /// <summary>
 30    /// The local manifest, which includes currently installed components.
 31    /// </summary>
 32    public IEnumerable<Component> Local
 33    {
 034        get => _local;
 035        private set => _local = value.ToList();
 36    }
 37
 38    /// <summary>
 39    /// The remote manifest, which includes available components to be downloaded.
 40    /// </summary>
 41    public IEnumerable<Component> Remote
 42    {
 043        get => _remote;
 044        private set => _remote = value.ToList();
 45    }
 46
 47    /// <summary>
 48    /// The merged manifest, which is the local and remote manifests merged into one list.
 49    /// Installed components can be told apart by verifying whether <c>Installation != null</c>.
 50    /// </summary>
 51    public IEnumerable<Component> Merged
 52    {
 053        get => _merged;
 054        private set => _merged = value.ToList();
 55    }
 56
 57    public Task Initialize(bool includeUnsupported = false, bool includePrerelease = false,
 58        CancellationToken cancellationToken = default)
 059    {
 060        _includeUnsupported = includeUnsupported;
 061        _includePrerelease = includePrerelease;
 062        return Refresh(cancellationToken);
 063    }
 64
 65    public async Task Add(Component component, CancellationToken cancellationToken = default)
 066    {
 067        component.Installation = new Installation(DateTimeOffset.UtcNow);
 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, bool includePrerelease = fals
 27        CancellationToken cancellationToken = default)
 028    {
 029        var manifestSnapRootPath = Path.Join("/", "snap", "dotnet-manifest", "current");
 30
 031        var filesToRead = new List<string>
 032        {
 033            Path.Join(manifestSnapRootPath, "supported.json")
 034        };
 35
 036        if (includeUnsupported)
 037        {
 038            filesToRead.Add(Path.Join(manifestSnapRootPath, "unsupported.json"));
 039        }
 40
 041        if (includePrerelease)
 042        {
 043            filesToRead.Add(Path.Join(manifestSnapRootPath, "previews.json"));
 044        }
 45
 046        var components = new List<Component>();
 047        foreach (var contentStream in filesToRead.Select(File.OpenRead))
 048        {
 049            var currentComponents = await JsonSerializer.DeserializeAsync<List<Component>>(
 050                contentStream,
 051                JsonSerializerOptions,
 052                cancellationToken: cancellationToken);
 53
 054            if (currentComponents is null) continue;
 55
 056            components.AddRange(currentComponents);
 057        }
 58
 059        return components;
 060    }
 61
 62    private async Task Refresh(CancellationToken cancellationToken = default)
 063    {
 064        _local = await LoadLocal(cancellationToken);
 065        _remote = await LoadRemote(_includeUnsupported, _includePrerelease, cancellationToken);
 066        _merged = MergeManifests(_remote, _local);
 067    }
 68
 69    private static List<Component> MergeManifests(List<Component> remoteComponents, List<Component> localComponents)
 070    {
 071        var result = new List<Component>();
 072        result.AddRange(remoteComponents);
 73
 074        foreach (var localComponent in localComponents)
 075        {
 076            if (result.All(c => c.Key != localComponent.Key))
 077            {
 78                // Local component is not in remote, add it
 079                result.Add(localComponent);
 080            }
 81            else
 082            {
 83                // Local component exists in remote. Take it as source of truth and update installation info
 084                var remote = result.First(c => c.Key == localComponent.Key);
 085                remote.Installation = localComponent.Installation;
 086            }
 087        }
 88
 089        return result;
 090    }
 91
 92    private async Task Save(CancellationToken cancellationToken = default)
 093    {
 094        await using var sw = new StreamWriter(LocalManifestPath, append: false, Encoding.UTF8);
 095        var content = JsonSerializer.Serialize(_local, JsonSerializerOptions);
 096        var stringBuilder = new StringBuilder(content);
 097        await sw.WriteLineAsync(stringBuilder, cancellationToken);
 098    }
 99}