Skip to content

Commit dd9da5e

Browse files
Copilotazchohfi
andcommitted
Add manifest update-assets command for generating image assets
- Add SkiaSharp library for cross-platform image processing - Create IImageAssetService and ImageAssetService for generating MSIX assets - Implement ManifestUpdateAssetsCommand to update manifest assets from a source image - Generate all 12 required asset sizes (logos, splash screens, tiles) - Add comprehensive tests for the new command - Support for both Windows and Linux environments (for CI/CD) Co-authored-by: azchohfi <[email protected]>
1 parent 0a4541b commit dd9da5e

File tree

9 files changed

+548
-1
lines changed

9 files changed

+548
-1
lines changed

src/winapp-CLI/Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
66
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" />
77
<PackageVersion Include="Microsoft.Telemetry.Inbox.Managed" Version="10.0.25148.1001-220626-1600.rs-fun-deploy-dev5" />
8+
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
9+
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
810
</ItemGroup>
911
</Project>
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using WinApp.Cli.Commands;
5+
6+
namespace WinApp.Cli.Tests;
7+
8+
[TestClass]
9+
public class ManifestUpdateAssetsCommandTests : BaseCommandTests
10+
{
11+
private string _testManifestPath = null!;
12+
private string _testImagePath = null!;
13+
14+
[TestInitialize]
15+
public void Setup()
16+
{
17+
// Create a test manifest file
18+
_testManifestPath = Path.Combine(_tempDirectory.FullName, "appxmanifest.xml");
19+
CreateTestManifest(_testManifestPath);
20+
21+
// Create a test image file
22+
_testImagePath = Path.Combine(_tempDirectory.FullName, "testlogo.png");
23+
CreateTestImage(_testImagePath);
24+
}
25+
26+
private void CreateTestManifest(string path)
27+
{
28+
var manifestContent = @"<?xml version=""1.0"" encoding=""utf-8""?>
29+
<Package
30+
xmlns=""http://schemas.microsoft.com/appx/manifest/foundation/windows10""
31+
xmlns:uap=""http://schemas.microsoft.com/appx/manifest/uap/windows10"">
32+
<Identity Name=""TestPackage"" Publisher=""CN=TestPublisher"" Version=""1.0.0.0"" />
33+
<Properties>
34+
<DisplayName>TestPackage</DisplayName>
35+
<PublisherDisplayName>TestPublisher</PublisherDisplayName>
36+
<Logo>Assets\StoreLogo.png</Logo>
37+
</Properties>
38+
<Applications>
39+
<Application Id=""TestApp"" Executable=""test.exe"">
40+
<uap:VisualElements
41+
DisplayName=""TestPackage""
42+
Description=""Test Application""
43+
BackgroundColor=""transparent""
44+
Square150x150Logo=""Assets\Square150x150Logo.png""
45+
Square44x44Logo=""Assets\Square44x44Logo.png"">
46+
<uap:DefaultTile Wide310x150Logo=""Assets\Wide310x150Logo.png"" />
47+
</uap:VisualElements>
48+
</Application>
49+
</Applications>
50+
</Package>";
51+
File.WriteAllText(path, manifestContent);
52+
}
53+
54+
private void CreateTestImage(string path)
55+
{
56+
// Create a minimal valid PNG file (1x1 pixel transparent image)
57+
var pngData = new byte[]
58+
{
59+
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
60+
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
61+
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 dimensions
62+
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, // RGBA, no compression
63+
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk
64+
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, // Image data
65+
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, // Image data
66+
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, // IEND chunk
67+
0x42, 0x60, 0x82
68+
};
69+
File.WriteAllBytes(path, pngData);
70+
}
71+
72+
[TestMethod]
73+
public void ManifestUpdateAssetsCommandShouldBeAvailable()
74+
{
75+
// Arrange & Act
76+
var manifestCommand = GetRequiredService<ManifestCommand>();
77+
78+
// Assert
79+
Assert.IsNotNull(manifestCommand, "ManifestCommand should be created");
80+
Assert.IsTrue(manifestCommand.Subcommands.Any(c => c.Name == "update-assets"),
81+
"Should have 'update-assets' subcommand");
82+
}
83+
84+
[TestMethod]
85+
public async Task ManifestUpdateAssetsCommandShouldGenerateAssets()
86+
{
87+
// Arrange
88+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
89+
var args = new[]
90+
{
91+
_testManifestPath,
92+
_testImagePath
93+
};
94+
95+
// Act
96+
var parseResult = updateAssetsCommand.Parse(args);
97+
var exitCode = await parseResult.InvokeAsync();
98+
99+
// Assert
100+
Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully");
101+
102+
// Verify Assets directory was created
103+
var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets");
104+
Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created");
105+
106+
// Verify at least some of the required assets were generated
107+
var expectedAssets = new[]
108+
{
109+
"Square44x44Logo.png",
110+
"Square150x150Logo.png",
111+
"Wide310x150Logo.png",
112+
"SplashScreen.png",
113+
"StoreLogo.png"
114+
};
115+
116+
foreach (var asset in expectedAssets)
117+
{
118+
var assetPath = Path.Combine(assetsDir, asset);
119+
Assert.IsTrue(File.Exists(assetPath), $"Asset {asset} should be generated");
120+
}
121+
}
122+
123+
[TestMethod]
124+
public void ManifestUpdateAssetsCommandShouldFailWithNonExistentManifest()
125+
{
126+
// Arrange
127+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
128+
var nonExistentManifest = Path.Combine(_tempDirectory.FullName, "nonexistent.xml");
129+
var args = new[]
130+
{
131+
nonExistentManifest,
132+
_testImagePath
133+
};
134+
135+
// Act
136+
var parseResult = updateAssetsCommand.Parse(args);
137+
138+
// Assert
139+
Assert.IsNotEmpty(parseResult.Errors, "Should have parse errors for non-existent manifest");
140+
}
141+
142+
[TestMethod]
143+
public void ManifestUpdateAssetsCommandShouldFailWithNonExistentImage()
144+
{
145+
// Arrange
146+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
147+
var nonExistentImage = Path.Combine(_tempDirectory.FullName, "nonexistent.png");
148+
var args = new[]
149+
{
150+
_testManifestPath,
151+
nonExistentImage
152+
};
153+
154+
// Act
155+
var parseResult = updateAssetsCommand.Parse(args);
156+
157+
// Assert
158+
Assert.IsNotEmpty(parseResult.Errors, "Should have parse errors for non-existent image");
159+
}
160+
161+
[TestMethod]
162+
public async Task ManifestUpdateAssetsCommandShouldGenerateCorrectSizes()
163+
{
164+
// Arrange
165+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
166+
var args = new[]
167+
{
168+
_testManifestPath,
169+
_testImagePath
170+
};
171+
172+
// Act
173+
var parseResult = updateAssetsCommand.Parse(args);
174+
var exitCode = await parseResult.InvokeAsync();
175+
176+
// Assert
177+
Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully");
178+
179+
// Verify specific asset sizes
180+
var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets");
181+
182+
// Check that scale-200 assets exist (which should be 2x the base size)
183+
Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "Square44x44Logo.scale-200.png")),
184+
"Square44x44Logo.scale-200.png should exist");
185+
Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "Square150x150Logo.scale-200.png")),
186+
"Square150x150Logo.scale-200.png should exist");
187+
}
188+
189+
[TestMethod]
190+
public async Task ManifestUpdateAssetsCommandShouldOverwriteExistingAssets()
191+
{
192+
// Arrange
193+
var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets");
194+
Directory.CreateDirectory(assetsDir);
195+
196+
// Create a dummy existing asset
197+
var existingAssetPath = Path.Combine(assetsDir, "Square150x150Logo.png");
198+
File.WriteAllText(existingAssetPath, "old content");
199+
var oldLength = new FileInfo(existingAssetPath).Length;
200+
201+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
202+
var args = new[]
203+
{
204+
_testManifestPath,
205+
_testImagePath
206+
};
207+
208+
// Act
209+
var parseResult = updateAssetsCommand.Parse(args);
210+
var exitCode = await parseResult.InvokeAsync();
211+
212+
// Assert
213+
Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully");
214+
Assert.IsTrue(File.Exists(existingAssetPath), "Asset should still exist");
215+
216+
var newLength = new FileInfo(existingAssetPath).Length;
217+
Assert.AreNotEqual(oldLength, newLength, "Asset should be overwritten with new content");
218+
}
219+
220+
[TestMethod]
221+
public void ManifestUpdateAssetsCommandHelpShouldDisplayCorrectInformation()
222+
{
223+
// Arrange
224+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
225+
var args = new[] { "--help" };
226+
227+
// Act
228+
var parseResult = updateAssetsCommand.Parse(args);
229+
230+
// Assert
231+
Assert.IsNotNull(parseResult, "Parse result should not be null");
232+
// The help option should be recognized and not produce errors
233+
}
234+
235+
[TestMethod]
236+
public async Task ManifestUpdateAssetsCommandShouldLogProgress()
237+
{
238+
// Arrange
239+
var updateAssetsCommand = GetRequiredService<ManifestUpdateAssetsCommand>();
240+
var args = new[]
241+
{
242+
_testManifestPath,
243+
_testImagePath,
244+
"--verbose"
245+
};
246+
247+
// Act
248+
var parseResult = updateAssetsCommand.Parse(args);
249+
var exitCode = await parseResult.InvokeAsync();
250+
251+
// Assert
252+
Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully");
253+
254+
var output = ConsoleStdOut.ToString();
255+
Assert.Contains("Updating assets", output, "Should log update message");
256+
Assert.Contains("generated", output.ToLowerInvariant(), "Should log generation progress");
257+
}
258+
}

src/winapp-CLI/WinApp.Cli.Tests/WinApp.Cli.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@
2121
<ProjectReference Include="..\WinApp.Cli\WinApp.Cli.csproj" />
2222
</ItemGroup>
2323

24+
<ItemGroup>
25+
<!-- Include SkiaSharp.NativeAssets.Linux for CI/CD environments running on Linux -->
26+
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Condition="$([MSBuild]::IsOSPlatform('Linux'))" />
27+
</ItemGroup>
28+
2429
</Project>

src/winapp-CLI/WinApp.Cli/Commands/ManifestCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ namespace WinApp.Cli.Commands;
77

88
internal class ManifestCommand : Command
99
{
10-
public ManifestCommand(ManifestGenerateCommand manifestGenerateCommand)
10+
public ManifestCommand(ManifestGenerateCommand manifestGenerateCommand, ManifestUpdateAssetsCommand manifestUpdateAssetsCommand)
1111
: base("manifest", "AppxManifest.xml management")
1212
{
1313
Subcommands.Add(manifestGenerateCommand);
14+
Subcommands.Add(manifestUpdateAssetsCommand);
1415
}
1516
}

0 commit comments

Comments
 (0)