Skip to content

Commit 22eb081

Browse files
committed
ci: bootstrap Devolutions#5311 backport
1 parent 9f17f4f commit 22eb081

1 file changed

Lines changed: 290 additions & 0 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
name: Bootstrap upstream #5311 backport
2+
3+
on:
4+
push:
5+
branches:
6+
- classic-test-upstream-backports-3
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
apply:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
ref: classic-test-upstream-backports-3
19+
20+
- name: Build clean backport commit
21+
shell: bash
22+
run: |
23+
set -euo pipefail
24+
base="$(git rev-parse "$GITHUB_SHA^")"
25+
git reset --hard "$base"
26+
27+
python3 - <<'PY'
28+
from pathlib import Path
29+
30+
package_path = Path('src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs')
31+
package = package_path.read_text()
32+
33+
old = ' private static readonly ConcurrentDictionary<int, Uri?> _cachedIconPaths = new();\n'
34+
new = ''' private static readonly ConcurrentDictionary<long, Uri> _cachedIconPaths = new();
35+
private static readonly ConcurrentDictionary<long, long> _failedIconLookups = new();
36+
private static readonly TimeSpan _iconLookupRetryInterval = TimeSpan.FromMinutes(5);
37+
38+
public static TimeSpan? TEST_IconLookupRetryIntervalOverride { private get; set; }
39+
40+
private static TimeSpan IconLookupRetryInterval =>
41+
TEST_IconLookupRetryIntervalOverride ?? _iconLookupRetryInterval;
42+
'''
43+
assert old in package
44+
package = package.replace(old, new, 1)
45+
46+
old = ''' public virtual Uri? GetIconUrlIfAny()
47+
{
48+
if (_cachedIconPaths.TryGetValue(this.GetHashCode(), out Uri? path))
49+
{
50+
return path;
51+
}
52+
var CachedIcon = LoadIconUrlIfAny();
53+
_cachedIconPaths.TryAdd(this.GetHashCode(), CachedIcon);
54+
return CachedIcon;
55+
}
56+
'''
57+
new = ''' public virtual Uri? GetIconUrlIfAny()
58+
{
59+
long cacheKey = _versionedHash;
60+
if (_cachedIconPaths.TryGetValue(cacheKey, out Uri? path))
61+
{
62+
return path;
63+
}
64+
65+
if (
66+
_failedIconLookups.TryGetValue(cacheKey, out long failedAt)
67+
&& Environment.TickCount64 - failedAt
68+
< (long)IconLookupRetryInterval.TotalMilliseconds
69+
)
70+
{
71+
return null;
72+
}
73+
74+
var CachedIcon = LoadIconUrlIfAny();
75+
if (CachedIcon is null)
76+
{
77+
_failedIconLookups[cacheKey] = Environment.TickCount64;
78+
return null;
79+
}
80+
81+
_failedIconLookups.TryRemove(cacheKey, out _);
82+
_cachedIconPaths[cacheKey] = CachedIcon;
83+
return CachedIcon;
84+
}
85+
'''
86+
assert old in package
87+
package = package.replace(old, new, 1)
88+
89+
old = ''' public static void ResetIconCache()
90+
{
91+
_cachedIconPaths.Clear();
92+
}
93+
'''
94+
new = ''' public static void ResetIconCache()
95+
{
96+
_cachedIconPaths.Clear();
97+
_failedIconLookups.Clear();
98+
}
99+
'''
100+
assert old in package
101+
package = package.replace(old, new, 1)
102+
package_path.write_text(package)
103+
104+
icon_path = Path('src/UniGetUI.Core.IconStore/IconCacheEngine.cs')
105+
icon = icon_path.read_text()
106+
assert icon.count('image/image/x-icon') == 2
107+
icon = icon.replace('image/image/x-icon', 'image/x-icon')
108+
icon_path.write_text(icon)
109+
110+
test_path = Path('src/UniGetUI.PackageEngine.Tests/PackageIconLookupTests.cs')
111+
test_path.write_text('''using UniGetUI.Core.Data;
112+
using UniGetUI.Core.IconEngine;
113+
using UniGetUI.Core.SettingsEngine;
114+
using UniGetUI.Core.SettingsEngine.SecureSettings;
115+
using UniGetUI.PackageEngine.Interfaces;
116+
using UniGetUI.PackageEngine.PackageClasses;
117+
using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
118+
119+
namespace UniGetUI.PackageEngine.Tests;
120+
121+
public sealed class PackageIconLookupTests : IDisposable
122+
{
123+
private readonly string _testRoot;
124+
125+
public PackageIconLookupTests()
126+
{
127+
_testRoot = Path.Combine(
128+
Path.GetTempPath(),
129+
nameof(PackageIconLookupTests),
130+
Guid.NewGuid().ToString("N")
131+
);
132+
CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
133+
SecureSettings.TEST_SecureSettingsRootOverride = Path.Combine(_testRoot, "SecureSettings");
134+
Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
135+
Settings.ResetSettings();
136+
Package.ResetIconCache();
137+
}
138+
139+
public void Dispose()
140+
{
141+
Package.TEST_IconLookupRetryIntervalOverride = null;
142+
Package.ResetIconCache();
143+
Settings.ResetSettings();
144+
CoreData.TEST_DataDirectoryOverride = null;
145+
SecureSettings.TEST_SecureSettingsRootOverride = null;
146+
if (Directory.Exists(_testRoot))
147+
Directory.Delete(_testRoot, recursive: true);
148+
}
149+
150+
[Fact]
151+
public void GetIconUrlIfAny_DoesNotRetryFailedLookupWithinRetryInterval()
152+
{
153+
int lookups = 0;
154+
var package = BuildPackage(
155+
"Contoso.WithinInterval",
156+
_ =>
157+
{
158+
lookups++;
159+
return null;
160+
}
161+
);
162+
163+
Assert.Null(package.GetIconUrlIfAny());
164+
Assert.Null(package.GetIconUrlIfAny());
165+
166+
Assert.Equal(1, lookups);
167+
}
168+
169+
[Fact]
170+
public void GetIconUrlIfAny_RetriesFailedLookupAfterRetryInterval()
171+
{
172+
Package.TEST_IconLookupRetryIntervalOverride = TimeSpan.Zero;
173+
string iconPath = CreateIconFile();
174+
175+
int lookups = 0;
176+
var package = BuildPackage(
177+
"Contoso.AfterInterval",
178+
_ =>
179+
{
180+
lookups++;
181+
return lookups == 1 ? null : new CacheableIcon(iconPath);
182+
}
183+
);
184+
185+
Assert.Null(package.GetIconUrlIfAny());
186+
Uri? retried = package.GetIconUrlIfAny();
187+
188+
Assert.Equal(2, lookups);
189+
Assert.NotNull(retried);
190+
Assert.True(retried.IsFile);
191+
Assert.Equal(iconPath, retried.LocalPath);
192+
}
193+
194+
[Fact]
195+
public void GetIconUrlIfAny_DoesNotResolveResolvedIconAgain()
196+
{
197+
Package.TEST_IconLookupRetryIntervalOverride = TimeSpan.Zero;
198+
string iconPath = CreateIconFile();
199+
200+
int lookups = 0;
201+
var package = BuildPackage(
202+
"Contoso.AlreadyResolved",
203+
_ =>
204+
{
205+
lookups++;
206+
return new CacheableIcon(iconPath);
207+
}
208+
);
209+
210+
Uri? first = package.GetIconUrlIfAny();
211+
Uri? second = package.GetIconUrlIfAny();
212+
213+
Assert.Equal(1, lookups);
214+
Assert.Equal(first, second);
215+
}
216+
217+
[Fact]
218+
public void ResetIconCache_AllowsFailedLookupToBeRetriedImmediately()
219+
{
220+
int lookups = 0;
221+
var package = BuildPackage(
222+
"Contoso.AfterReset",
223+
_ =>
224+
{
225+
lookups++;
226+
return null;
227+
}
228+
);
229+
230+
Assert.Null(package.GetIconUrlIfAny());
231+
Package.ResetIconCache();
232+
Assert.Null(package.GetIconUrlIfAny());
233+
234+
Assert.Equal(2, lookups);
235+
}
236+
237+
private string CreateIconFile()
238+
{
239+
Directory.CreateDirectory(_testRoot);
240+
string iconPath = Path.Combine(_testRoot, "icon.png");
241+
File.WriteAllBytes(iconPath, [0x89, 0x50, 0x4E, 0x47]);
242+
return iconPath;
243+
}
244+
245+
private static Package BuildPackage(string id, Func<IPackage, CacheableIcon?> iconFactory)
246+
{
247+
var manager = new PackageManagerBuilder()
248+
.ConfigureCapabilities(capabilities =>
249+
{
250+
capabilities.SupportsCustomPackageIcons = true;
251+
return capabilities;
252+
})
253+
.ConfigureDetails(details => details.IconFactory = iconFactory)
254+
.Build();
255+
256+
return new PackageBuilder().WithId(id).WithManager(manager).Build();
257+
}
258+
}
259+
''')
260+
261+
ledger_path = Path('maintenance/backports.yml')
262+
ledger = ledger_path.read_text()
263+
marker = 'backports:\n'
264+
assert marker in ledger
265+
entry = '''backports:
266+
- commit: 460178a3e99361946e570025a3a8e0c8b0257336
267+
upstream_pr: 5311
268+
status: applied-semantic
269+
class: backend-shared-icon-cache
270+
reason: Retries transient package icon lookup failures after five minutes, caches successful resolutions by full versioned hash, and fixes the x-icon MIME mapping. Avalonia bitmap-cache changes are omitted because Classic WinUI only stores icon URLs after successful lookup.
271+
272+
'''
273+
ledger = ledger.replace(marker, entry, 1)
274+
ledger_path.write_text(ledger)
275+
PY
276+
277+
git add maintenance/backports.yml \
278+
src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs \
279+
src/UniGetUI.Core.IconStore/IconCacheEngine.cs \
280+
src/UniGetUI.PackageEngine.Tests/PackageIconLookupTests.cs
281+
282+
test "$(git diff --cached --name-only | wc -l)" -eq 4
283+
! git diff --cached --name-only | grep -q '^src/UniGetUI.Avalonia/'
284+
285+
git config user.name 'github-actions[bot]'
286+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
287+
git commit -m 'backport: retry transient package icon failures' \
288+
-m 'Upstream-Commit: 460178a3e99361946e570025a3a8e0c8b0257336' \
289+
-m 'Upstream-PR: Devolutions/UniGetUI#5311'
290+
git push --force-with-lease origin HEAD:classic-test-upstream-backports-3

0 commit comments

Comments
 (0)