• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 17865799316

19 Sep 2025 05:47PM UTC coverage: 74.397% (+0.2%) from 74.179%
17865799316

Pull #4441

github

web-flow
Merge 8a2699706 into e49348427
Pull Request #4441: Don't clone with symlinks on Linux

5140 of 7262 branches covered (70.78%)

Branch coverage included in aggregate %.

5 of 8 new or added lines in 2 files covered. (62.5%)

11 existing lines in 4 files now uncovered.

11060 of 14513 relevant lines covered (76.21%)

1.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.06
/Core/Repositories/AvailableModule.cs
1
using System;
2
using System.IO;
3
using System.Text;
4
using System.Collections.Generic;
5
using System.Diagnostics;
6
using System.Linq;
7
using System.Runtime.Serialization;
8

9
using log4net;
10
using Newtonsoft.Json;
11
using Newtonsoft.Json.Serialization;
12

13
using CKAN.Configuration;
14
using CKAN.Versioning;
15
using CKAN.Extensions;
16

17
namespace CKAN
18
{
19
    /// <summary>
20
    /// Utility class to track version -> module mappings
21
    /// </summary>
22
    /// <remarks>
23
    /// Json must not contain AvailableModules which are empty
24
    /// </remarks>
25
    public class AvailableModule
26
    {
27
        [JsonIgnore]
28
        private string identifier;
29

30
        /// <param name="identifier">The module to keep track of</param>
31
        [JsonConstructor]
32
        private AvailableModule(string identifier)
2✔
33
        {
2✔
34
            this.identifier = identifier;
2✔
35
        }
2✔
36

37
        public AvailableModule(string identifier, IEnumerable<CkanModule> modules)
38
            : this(identifier)
2✔
39
        {
2✔
40
            foreach (var module in modules)
5✔
41
            {
2✔
42
                Add(module);
2✔
43
            }
2✔
44
        }
2✔
45

46
        [OnDeserialized]
47
        internal void DeserialisationFixes(StreamingContext context)
48
        {
2✔
49
            identifier = module_version.Values.Select(m => m.identifier)
2✔
50
                                              .Last();
51
            Debug.Assert(module_version.Values.All(m => identifier.Equals(m.identifier)));
2✔
52
        }
2✔
53

54
        /// <summary>
55
        /// Generate a new AvailableModule given its CkanModules
56
        /// </summary>
57
        /// <param name="availMods">Sequence of mods to be contained, expected to be IGrouping&lt;&gt;, so it should support O(1) Count(), even though IEnumerable&lt;&gt; in general does not</param>
58
        /// <returns></returns>
59
        public static AvailableModule Merge(IEnumerable<AvailableModule> availMods)
60
            => availMods.Count() == 1 ? availMods.First()
2!
61
                                      : new AvailableModule(availMods.First().identifier,
62
                                                            availMods.Reverse().SelectMany(am => am.AllAvailable()));
2✔
63

64
        // The map of versions -> modules, that's what we're about!
65
        // First element is the oldest version, last is the newest.
66
        [JsonProperty]
67
        [JsonConverter(typeof(JsonLeakySortedDictionaryConverter<ModuleVersion, CkanModule>))]
68
        internal SortedDictionary<ModuleVersion, CkanModule> module_version =
2✔
69
            new SortedDictionary<ModuleVersion, CkanModule>();
70

71
        [OnError]
72
        #pragma warning disable IDE0051, IDE0060
73
        private static void OnError(StreamingContext context, ErrorContext errorContext)
74
        #pragma warning restore IDE0051, IDE0060
75
        {
×
76
            log.WarnFormat("Discarding CkanModule, failed to parse {0}: {1}",
×
77
                errorContext.Path, errorContext.Error.GetBaseException().Message);
78
            errorContext.Handled = true;
×
79
        }
×
80

81
        /// <summary>
82
        /// Record the given module version as being available.
83
        /// </summary>
84
        private void Add(CkanModule module)
85
        {
2✔
86
            if (!module.identifier.Equals(identifier))
2!
87
            {
×
88
                throw new ArgumentException(
×
89
                    string.Format("This AvailableModule is for tracking {0} not {1}", identifier, module.identifier));
90
            }
91

92
            log.DebugFormat("Adding to available module: {0}", module);
2✔
93
            module_version[module.version] = module;
2✔
94
        }
2✔
95

96
        /// <summary>
97
        /// Return the most recent release of a module with a optional ksp version to target and a RelationshipDescriptor to satisfy.
98
        /// </summary>
99
        /// <param name="stabilityTolerance">Stability tolerance that the module must match.</param>
100
        /// <param name="ksp_version">If not null only consider mods which match this ksp version.</param>
101
        /// <param name="relationship">If not null only consider mods which satisfy the RelationshipDescriptor.</param>
102
        /// <param name="installed">Modules that are already installed</param>
103
        /// <param name="toInstall">Modules that are planned to be installed</param>
104
        /// <returns></returns>
105
        public CkanModule? Latest(StabilityToleranceConfig         stabilityTolerance,
106
                                  GameVersionCriteria?             ksp_version  = null,
107
                                  RelationshipDescriptor?          relationship = null,
108
                                  IReadOnlyCollection<CkanModule>? installed    = null,
109
                                  IReadOnlyCollection<CkanModule>? toInstall    = null)
110
            => Latest(stabilityTolerance.ModStabilityTolerance(identifier)
2✔
111
                      ?? stabilityTolerance.OverallStabilityTolerance,
112
                      ksp_version, relationship, installed, toInstall);
113

114
        public CkanModule? Latest(ReleaseStatus                    stabilityTolerance,
115
                                  GameVersionCriteria?             ksp_version  = null,
116
                                  RelationshipDescriptor?          relationship = null,
117
                                  IReadOnlyCollection<CkanModule>? installed    = null,
118
                                  IReadOnlyCollection<CkanModule>? toInstall    = null)
119
        {
2✔
120
            var modules = module_version.Values
2✔
121
                                        .Where(m => m.release_status <= stabilityTolerance)
2✔
122
                                        .Reverse();
123
            if (relationship != null)
2✔
124
            {
2✔
125
                modules = modules.Where(relationship.WithinBounds);
2✔
126
            }
2✔
127
            if (ksp_version != null)
2✔
128
            {
2✔
129
                modules = modules.Where(m => m.IsCompatible(ksp_version));
2✔
130
            }
2✔
131
            if (installed != null)
2✔
132
            {
2✔
133
                modules = modules.Where(m => DependsAndConflictsOK(m, installed));
2✔
134
            }
2✔
135
            if (toInstall != null)
2✔
136
            {
2✔
137
                modules = modules.Where(m => DependsAndConflictsOK(m, toInstall));
2✔
138
            }
2✔
139
            return modules.FirstOrDefault();
2✔
140
        }
2✔
141

142
        public static bool DependsAndConflictsOK(CkanModule                      module,
143
                                                 IReadOnlyCollection<CkanModule> others)
144
        {
2✔
145
            if (module.depends != null)
2✔
146
            {
2✔
147
                foreach (RelationshipDescriptor rel in module.depends)
5✔
148
                {
2✔
149
                    // If 'others' matches an identifier, it must also match the versions, else fail
150
                    if (rel.ContainsAny(others.Select(m => m.identifier)) && !rel.MatchesAny(others, null, null))
2✔
151
                    {
2✔
152
                        log.DebugFormat("Unsatisfied dependency {0}, rejecting {1}", rel, module);
2✔
153
                        return false;
2✔
154
                    }
155
                }
2✔
156
            }
2✔
157
            var othersMinusSelf = others.Where(m => m.identifier != module.identifier).ToList();
2✔
158
            if (module.conflicts != null)
2✔
159
            {
2✔
160
                // Skip self-conflicts (but catch other modules providing self)
161
                foreach (RelationshipDescriptor rel in module.conflicts)
5✔
162
                {
2✔
163
                    // If any of the conflicts are present, fail
164
                    if (rel.MatchesAny(othersMinusSelf, null, null, out CkanModule? matched))
2✔
165
                    {
2✔
166
                        log.DebugFormat("Found conflict with {0}, rejecting {1}", matched, module);
2✔
167
                        return false;
2✔
168
                    }
169
                }
2✔
170
            }
2✔
171
            // Check reverse conflicts so user isn't prompted to choose modules that will error out immediately
172
            var selfArray = new CkanModule[] { module };
2✔
173
            foreach (CkanModule other in othersMinusSelf)
5✔
174
            {
2✔
175
                if (other.conflicts != null)
2✔
176
                {
2✔
177
                    foreach (RelationshipDescriptor rel in other.conflicts)
5✔
178
                    {
2✔
179
                        if (rel.MatchesAny(selfArray, null, null))
2✔
180
                        {
2✔
181
                            log.DebugFormat("Found reverse conflict with {0}, rejecting {1}", other, module);
2✔
182
                            return false;
2✔
183
                        }
184
                    }
2✔
185
                }
2✔
186
                // And check reverse depends for version limits
187
                if (other.depends != null)
2✔
188
                {
2✔
189
                    foreach (RelationshipDescriptor rel in other.depends)
5✔
190
                    {
2✔
191
                        // If 'others' matches an identifier, it must also match the versions, else fail
192
                        if (rel.ContainsAny(Enumerable.Repeat(module.identifier, 1))
2✔
193
                            && !rel.MatchesAny(selfArray, null, null))
194
                        {
2✔
195
                            log.DebugFormat("Unmatched reverse dependency from {0}, rejecting", other);
2✔
196
                            return false;
2✔
197
                        }
198
                    }
2✔
199
                }
2✔
200
            }
2✔
201
            return true;
2✔
202
        }
2✔
203

204
        /// <summary>
205
        /// Returns the latest game version that is compatible with this mod.
206
        /// Checks all versions of the mod.
207
        /// </summary>
208
        public GameVersion LatestCompatibleGameVersion(List<GameVersion> realVersions)
209
        {
2✔
210
            var ranges = module_version.Values
2✔
211
                                       .Select(m => new GameVersionRange(m.EarliestCompatibleGameVersion(),
2✔
212
                                                                         m.LatestCompatibleGameVersion()))
213
                                       .Memoize();
214
            if (ranges.Any(r => r.Upper.Value.IsAny))
2✔
215
            {
2✔
216
                // Can't get later than Any, so no need for more complex logic
217
                return realVersions?.LastOrDefault()
2!
218
                                   // This is needed for when we have no real versions loaded, such as tests
UNCOV
219
                                   ?? module_version.Values.Max(m => m.LatestCompatibleGameVersion())
×
220
                                   ?? module_version.Values.Last().LatestCompatibleGameVersion();
221
            }
222
            // Find the range with the highest upper bound
223
            var bestRange = ranges.Distinct()
2✔
224
                                  .Aggregate((best, r) => r.Upper == GameVersionBound.Highest(best.Upper, r.Upper)
2✔
225
                                                              ? r
226
                                                              : best);
227
            return realVersions?.LastOrDefault(bestRange.Contains)
2!
228
                               // This is needed for when we have no real versions loaded, such as tests
UNCOV
229
                               ?? module_version.Values.Max(m => m.LatestCompatibleGameVersion())
×
230
                               ?? module_version.Values.Last().LatestCompatibleGameVersion();
231
        }
2✔
232

233
        /// <summary>
234
        /// Returns the module with the specified version, or null if that does not exist.
235
        /// </summary>
236
        public CkanModule? ByVersion(ModuleVersion v)
237
            => module_version.TryGetValue(v, out CkanModule? module) ? module : null;
2✔
238

239
        /// <summary>
240
        /// Some code may expect this to be sorted in descending order
241
        /// </summary>
242
        public IEnumerable<CkanModule> AllAvailable()
243
            => module_version.Values.Reverse();
2✔
244

245
        /// <summary>
246
        /// Return the entire section of registry.json for this mod
247
        /// </summary>
248
        /// <returns>
249
        /// Nicely formatted JSON string containing metadata for all of this mod's available versions
250
        /// </returns>
251
        public string FullMetadata()
252
        {
×
253
            StringWriter sw = new StringWriter(new StringBuilder());
×
254
            using (JsonTextWriter writer = new JsonTextWriter(sw)
×
255
            {
256
                Formatting  = Formatting.Indented,
257
                Indentation = 4,
258
                IndentChar  = ' '
259
            })
260
            {
×
261
                new JsonSerializer().Serialize(writer, this);
×
262
            }
×
263
            return sw.ToString();
×
264
        }
×
265

266
        private static readonly ILog log = LogManager.GetLogger(typeof(AvailableModule));
2✔
267
    }
268
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc