feat(updater_tools): add diff command (#164)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user