feat(updater_tools): add diff command (#164)

This commit is contained in:
Bryan Oltman
2024-05-17 16:39:14 -04:00
committed by GitHub
parent 80937e6dd5
commit 6c3aa53334
6 changed files with 302 additions and 35 deletions
@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:updater_tools/src/commands/updater_tool_command.dart';
import 'package:updater_tools/src/extensions/arg_results.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
/// The arg name to specify the path to the release binary.
const releaseCliArg = 'release';
/// The arg name to specify the path to the patch binary.
const patchCliArg = 'patch';
/// The arg name to specify the path to the patch executable.
const patchExecutableCliArg = 'patch-executable';
/// The arg name to specify the output file.
const outputCliArg = 'output';
/// {@template diff_command}
/// A wrapper around the patch executable
/// {@endtemplate}
class DiffCommand extends UpdaterToolCommand {
/// {@macro diff_command}
DiffCommand([MakePatchPackager? makePatchPackager])
: _makePatchPackagerOverride = makePatchPackager,
super() {
argParser
..addOption(
releaseCliArg,
abbr: 'r',
mandatory: true,
help: 'The path to the release artifact which will be patched',
)
..addOption(
patchCliArg,
abbr: 'p',
mandatory: true,
help: 'The path to the patch artifact which will be packaged',
)
..addOption(
patchExecutableCliArg,
mandatory: true,
help:
'''The path to the patch executable that creates a binary diff between two files''',
)
..addOption(
outputCliArg,
abbr: 'o',
mandatory: true,
help: '''
Where to write the packaged patch archives.
This should be a directory, and will contain patch archives for each architecture.''',
);
}
final MakePatchPackager? _makePatchPackagerOverride;
@override
String get name => 'diff';
@override
String get description =>
'''Outputs a binary diff of the provided release and patch files, using release as a base.''';
@override
Future<int> run() async {
final File releaseFile;
final File patchFile;
final File patchExecutable;
try {
releaseFile = results.asExistingFile(releaseCliArg);
patchFile = results.asExistingFile(patchCliArg);
patchExecutable = results.asExistingFile(patchExecutableCliArg);
} catch (e) {
logger.err('$e');
return ExitCode.usage.code;
}
final patchPackager = (_makePatchPackagerOverride ?? PatchPackager.new)(
patchExecutable: patchExecutable,
);
await patchPackager.makeDiff(
base: releaseFile,
patch: patchFile,
outFile: File(results[outputCliArg] as String),
);
return ExitCode.success.code;
}
}
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:updater_tools/src/artifact_type.dart';
import 'package:updater_tools/src/commands/updater_tool_command.dart';
import 'package:updater_tools/src/extensions/arg_results.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
@@ -21,11 +22,6 @@ const patchExecutableCliArg = 'patch-executable';
/// The arg name to specify the output directory.
const outputCliArg = 'output';
/// Function signature for the [PatchPackager] constructor.
typedef MakePatchPackager = PatchPackager Function({
required File patchExecutable,
});
/// {@template package_patch_command}
/// A command to package patch artifacts.
/// {@endtemplate}
@@ -81,16 +77,18 @@ This should be a directory, and will contain patch archives for each architectur
@override
Future<int> run() async {
final releaseFile = File(results[releaseCliArg] as String);
final patchFile = File(results[patchCliArg] as String);
final patchExecutable = File(results[patchExecutableCliArg] as String);
final outputDirectory = Directory(results[outputCliArg] as String);
final archiveType = ArchiveType.values.byName(
results[archiveTypeCliArg] as String,
);
final File releaseFile;
final File patchFile;
final File patchExecutable;
try {
_assertCliArgsValid();
releaseFile = results.asExistingFile(releaseCliArg);
patchFile = results.asExistingFile(patchCliArg);
patchExecutable = results.asExistingFile(patchExecutableCliArg);
} catch (e) {
logger.err('$e');
return ExitCode.usage.code;
@@ -115,27 +113,4 @@ This should be a directory, and will contain patch archives for each architectur
return ExitCode.success.code;
}
/// Verifies that CLI arguments point to existing files. Throws an
/// [ArgumentError] if any of the args are not valid.
void _assertCliArgsValid() {
final releaseFilePath = results[releaseCliArg] as String;
final patchFilePath = results[patchCliArg] as String;
final patchExecutablePath = results[patchExecutableCliArg] as String;
_verifyFileExists(releaseFilePath, releaseCliArg);
_verifyFileExists(patchFilePath, patchCliArg);
_verifyFileExists(patchExecutablePath, patchExecutableCliArg);
}
/// Throws an [ArgumentError] if a file at [path] does not exist.
void _verifyFileExists(String path, String name) {
if (!File(path).existsSync()) {
throw ArgumentError.value(
path,
name,
'The $name file does not exist',
);
}
}
}
@@ -0,0 +1,19 @@
import 'dart:io';
import 'package:args/args.dart';
/// Extension methods for validating options provided to [ArgResults].
extension ArgResultsValidation on ArgResults {
File asExistingFile(String name) {
final file = File(this[name] as String);
if (!file.existsSync()) {
throw ArgumentError.value(
file.path,
name,
'The $name file does not exist',
);
}
return file;
}
}
@@ -22,6 +22,11 @@ class PackagingException implements Exception {
String toString() => 'PackagingException: $message';
}
/// Function signature for the [PatchPackager] constructor.
typedef MakePatchPackager = PatchPackager Function({
required File patchExecutable,
});
/// {@template patch_packager}
/// Creates and packages patch artifacts.
/// {@endtemplate }
@@ -120,7 +125,7 @@ class PatchPackager {
final diffArchDir = Directory(p.join(outDir.path, archName))
..createSync(recursive: true);
final diffFile = File(p.join(diffArchDir.path, 'dlc.vmcode'));
await _makeDiff(
await makeDiff(
base: releaseElf,
patch: patchElf,
outFile: diffFile,
@@ -149,7 +154,7 @@ class PatchPackager {
/// Create a binary diff between [base] and [patch]. Returns the path to the
/// diff file.
Future<void> _makeDiff({
Future<void> makeDiff({
required File base,
required File patch,
required File outFile,
@@ -2,6 +2,7 @@ import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:updater_tools/src/commands/commands.dart';
import 'package:updater_tools/src/commands/diff_command.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/version.dart';
@@ -30,7 +31,7 @@ class UpdaterToolsCommandRunner extends CommandRunner<int> {
help: 'Noisy logging, including all shell commands executed.',
);
// Add sub commands
addCommand(DiffCommand());
addCommand(PackagePatchCommand());
}
@@ -0,0 +1,174 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:scoped_deps/scoped_deps.dart';
import 'package:test/test.dart';
import 'package:updater_tools/src/commands/diff_command.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
import '../../matchers/matchers.dart';
class _MockArgResults extends Mock implements ArgResults {}
class _MockLogger extends Mock implements Logger {}
class _MockPatchPackager extends Mock implements PatchPackager {}
void main() {
group(DiffCommand, () {
late ArgResults argResults;
late Logger logger;
late PatchPackager patchPackager;
late DiffCommand command;
late File releaseFile;
late File patchFile;
late File patchExecutable;
late File outputFile;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
loggerRef.overrideWith(() => logger),
},
);
}
setUpAll(() {
registerFallbackValue(Directory(''));
registerFallbackValue(File(''));
});
setUp(() {
argResults = _MockArgResults();
logger = _MockLogger();
patchPackager = _MockPatchPackager();
final tempDir = Directory.systemTemp.createTempSync();
releaseFile = File(p.join(tempDir.path, 'release'))
..createSync(recursive: true);
patchFile = File(p.join(tempDir.path, 'patch'))
..createSync(recursive: true);
patchExecutable = File(p.join(tempDir.path, 'patch.exe'))
..createSync(recursive: true);
outputFile = File(p.join(tempDir.path, 'output'));
when(() => argResults[releaseCliArg]).thenReturn(releaseFile.path);
when(() => argResults[patchCliArg]).thenReturn(patchFile.path);
when(() => argResults[patchExecutableCliArg])
.thenReturn(patchExecutable.path);
when(() => argResults[outputCliArg]).thenReturn(outputFile.path);
command = DiffCommand(
({required File patchExecutable}) => patchPackager,
)..testArgResults = argResults;
});
test('has a non-empty name', () {
expect(command.name, isNotEmpty);
});
test('has a non-empty description', () {
expect(command.description, isNotEmpty);
});
group('arg validation', () {
group('when release file does not exist', () {
setUp(() {
releaseFile.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The release file does not exist')),
),
);
});
});
group('when patch file does not exist', () {
setUp(() {
patchFile.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The patch file does not exist')),
),
);
});
});
group('when patch executable does not exist', () {
setUp(() {
patchExecutable.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The patch-executable file does not exist')),
),
);
});
});
});
group('when args are valid', () {
setUp(() {
when(
() => patchPackager.makeDiff(
base: any(named: 'base'),
patch: any(named: 'patch'),
outFile: any(named: 'outFile'),
),
).thenAnswer((_) async {});
});
test('forwards values to patchPackager', () {
expect(
runWithOverrides(command.run),
completion(ExitCode.success.code),
);
verify(
() => patchPackager.makeDiff(
base: any(
named: 'base',
that: equalsFileSystemEntity(releaseFile),
),
patch: any(
named: 'patch',
that: equalsFileSystemEntity(patchFile),
),
outFile: any(
named: 'outFile',
that: equalsFileSystemEntity(outputFile),
),
),
).called(1);
});
});
});
}