Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions tools/Zippy.Tests/ZipServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.IO;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.Zip;
using NUnit.Framework;
using Zippy;

namespace Zippy.Tests;

[TestFixture]
public sealed class ZipServiceTests
{
[Test]
public void CreateZip_FlattensFilePathsAndRootsDirectories()
{
// Suppose the command is run as `zippy zip out.zip my-file.txt foo/some-file.txt baz/some-dir` in a directory containing:
// - my-file.txt
// - foo/some-file.txt
// - baz/some-dir/my-file.txt
// - baz/some-dir/sub/nested.txt
// We should create a zip file with expected entries.
string tempRoot = Directory.CreateTempSubdirectory("zippy-test-").FullName;
try
{
string myFile = Path.Join(tempRoot, "my-file.txt");
File.WriteAllText(myFile, "a");

string fooDir = Path.Join(tempRoot, "foo");
Directory.CreateDirectory(fooDir);
string fooSomeFile = Path.Join(fooDir, "some-file.txt");
File.WriteAllText(fooSomeFile, "b");

string bazSomeDir = Path.Join(tempRoot, "baz", "some-dir");
Directory.CreateDirectory(Path.Join(bazSomeDir, "sub"));
File.WriteAllText(Path.Join(bazSomeDir, "inner.txt"), "c");
File.WriteAllText(Path.Join(bazSomeDir, "sub", "nested.txt"), "d");

string zipPath = Path.Join(tempRoot, "out.zip");

var service = new ZipService();
service.CreateZip(zipPath, new List<string> { myFile, fooSomeFile, bazSomeDir });

using FileStream stream = File.OpenRead(zipPath);
using var zip = new ZipFile(stream);
List<string> entryNames = zip.Cast<ZipEntry>().Select(e => e.Name).ToList();

Assert.That(entryNames, Does.Contain("my-file.txt"));
Assert.That(entryNames, Does.Contain("some-file.txt"));
Assert.That(entryNames, Does.Contain("some-dir/inner.txt"));
Assert.That(entryNames, Does.Contain("some-dir/sub/nested.txt"));

Assert.That(entryNames, Does.Not.Contain("foo/some-file.txt"));
Assert.That(entryNames, Does.Not.Contain("baz/some-dir/inner.txt"));
}
finally
{
Directory.Delete(tempRoot, true);
}
}

[Test]
public void CreateZip_WhenZipAlreadyExists_Throws()
{
string tempRoot = Directory.CreateTempSubdirectory("zippy-test-").FullName;
try
{
string zipPath = Path.Join(tempRoot, "out.zip");
File.WriteAllText(zipPath, "already exists");

string inputFile = Path.Join(tempRoot, "a.txt");
File.WriteAllText(inputFile, "a");

var service = new ZipService();
Assert.Throws<IOException>(() => service.CreateZip(zipPath, new List<string> { inputFile }));
}
finally
{
Directory.Delete(tempRoot, true);
}
}

[Test]
public void CreateZip_WhenDuplicateEntryNames_Throws()
{
string tempRoot = Directory.CreateTempSubdirectory("zippy-test-").FullName;
try
{
string dir1 = Path.Join(tempRoot, "one");
string dir2 = Path.Join(tempRoot, "two");
Directory.CreateDirectory(dir1);
Directory.CreateDirectory(dir2);

string file1 = Path.Join(dir1, "dup.txt");
string file2 = Path.Join(dir2, "dup.txt");
File.WriteAllText(file1, "a");
File.WriteAllText(file2, "b");

string zipPath = Path.Join(tempRoot, "out.zip");

var service = new ZipService();
Assert.Throws<ArgumentException>(() => service.CreateZip(zipPath, new List<string> { file1, file2 }));
}
finally
{
Directory.Delete(tempRoot, true);
}
}

[Test]
public void ExtractZipToCurrentDirectory_WhenFileExists_ThrowsAndDoesNotOverwrite()
{
string tempRoot = Directory.CreateTempSubdirectory("zippy-test-").FullName;
string zipRoot = Directory.CreateTempSubdirectory("zippy-zip-").FullName;

string originalCwd = Directory.GetCurrentDirectory();
try
{
string existingPath = Path.Join(tempRoot, "a.txt");
File.WriteAllText(existingPath, "original");

string inputFile = Path.Join(zipRoot, "a.txt");
File.WriteAllText(inputFile, "new");
string zipPath = Path.Join(zipRoot, "out.zip");

var service = new ZipService();
service.CreateZip(zipPath, new List<string> { inputFile });

Directory.SetCurrentDirectory(tempRoot);

Assert.Throws<IOException>(() => service.ExtractZipToCurrentDirectory(zipPath));
Assert.That(File.ReadAllText(existingPath), Is.EqualTo("original"));
}
finally
{
Directory.SetCurrentDirectory(originalCwd);
Directory.Delete(tempRoot, true);
Directory.Delete(zipRoot, true);
}
}
}
20 changes: 20 additions & 0 deletions tools/Zippy.Tests/Zippy.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Zippy\Zippy.csproj" />
</ItemGroup>

</Project>
90 changes: 90 additions & 0 deletions tools/Zippy/Cli.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.Parsing;

namespace Zippy;

/// <summary>
/// Contains the System.CommandLine wiring for Zippy.
/// This is separate from <see cref="ZipService"/> so it can be unit tested.
///
/// The purpose of the Zippy program is to provide a helper to test the functionality of SharpZipLib.
/// </summary>
public static class Cli
{
public static Parser BuildParser()
{
var rootCommand = new RootCommand("Zippy: zip and unzip utility") { TreatUnmatchedTokensAsErrors = true };

var unzipCommand = new Command("unzip", "Extract a zip file into the current working directory");
var unzipZipPathArgument = new Argument<string>("zipFile", description: "The zip file to extract")
{
Arity = ArgumentArity.ExactlyOne,
};
unzipCommand.AddArgument(unzipZipPathArgument);

unzipCommand.SetHandler(
(string zipFilePath) =>
{
var zipService = new ZipService();
zipService.ExtractZipToCurrentDirectory(zipFilePath);
},
unzipZipPathArgument
);

var zipCommand = new Command("zip", "Create a zip file from files and directories");
var zipZipPathArgument = new Argument<string>("zipFile", description: "The zip file to create")
{
Arity = ArgumentArity.ExactlyOne,
};
var zipInputsArgument = new Argument<List<string>>(
"inputs",
description: "Files and directories to include in the zip (added as root items)"
)
{
Arity = ArgumentArity.OneOrMore,
};
zipCommand.AddArgument(zipZipPathArgument);
zipCommand.AddArgument(zipInputsArgument);

zipCommand.SetHandler(
(string zipFilePath, List<string> inputs) =>
{
var zipService = new ZipService();
zipService.CreateZip(zipFilePath, inputs);
},
zipZipPathArgument,
zipInputsArgument
);

rootCommand.AddCommand(unzipCommand);
rootCommand.AddCommand(zipCommand);

// System.CommandLine's default exception handler prints a stack trace.
// Override it so we only print a concise message and return exit code 1.
return new CommandLineBuilder(rootCommand)
.UseDefaults()
.UseExceptionHandler(
(Exception ex, InvocationContext context) =>
{
context.Console.Error.WriteLine(ex.Message);
context.ExitCode = 1;
},
1
)
.Build();
}

public static Task<int> RunAsync(string[] args, IConsole console)
{
ArgumentNullException.ThrowIfNull(args);
ArgumentNullException.ThrowIfNull(console);

Parser parser = BuildParser();
return parser.InvokeAsync(args, console);
}
}
4 changes: 4 additions & 0 deletions tools/Zippy/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.CommandLine.IO;
using Zippy;

return await Cli.RunAsync(args, new SystemConsole());
Loading
Loading