feat(shorebird_cli): show changed files when warning user of patch content differences (#704)
This commit is contained in:
@@ -2,7 +2,8 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/mf_reader.dart';
|
||||
|
||||
/// Finds differences between two AABs.
|
||||
@@ -25,16 +26,17 @@ import 'package:shorebird_cli/src/archive_analysis/mf_reader.dart';
|
||||
///
|
||||
/// See https://developer.android.com/guide/app-bundle/app-bundle-format for
|
||||
/// reference.
|
||||
class AabDiffer extends AndroidArchiveDiffer {
|
||||
class AabDiffer extends ArchiveDiffer {
|
||||
/// Returns a set of file paths whose hashes differ between the AABs at the
|
||||
/// provided paths.
|
||||
@override
|
||||
Set<String> changedFiles(String aabPath1, String aabPath2) {
|
||||
final mfContents1 = _metaInfMfContent(File(aabPath1));
|
||||
final mfContents2 = _metaInfMfContent(File(aabPath2));
|
||||
final mfEntries1 = MfReader.parse(mfContents1).toSet();
|
||||
final mfEntries2 = MfReader.parse(mfContents2).toSet();
|
||||
return mfEntries1.difference(mfEntries2).map((entry) => entry.name).toSet();
|
||||
FileSetDiff changedFiles(String oldAabPath, String newAabPath) {
|
||||
final oldMfContents = _metaInfMfContent(File(oldAabPath));
|
||||
final newMfContents = _metaInfMfContent(File(newAabPath));
|
||||
return FileSetDiff.fromPathHashes(
|
||||
oldPathHashes: MfReader.parse(oldMfContents),
|
||||
newPathHashes: MfReader.parse(newMfContents),
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads the contents of META-INF/MANIFEST.MF from an AAB.
|
||||
|
||||
@@ -2,7 +2,8 @@ import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
|
||||
/// Finds differences between two AABs.
|
||||
///
|
||||
@@ -18,20 +19,23 @@ import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
||||
/// https://developer.android.com/studio/projects/android-library.html#aar-contents
|
||||
/// for reference. Note that .aars produced by Flutter modules do not contain
|
||||
/// .jar files, so only asset and dart changes are possible.
|
||||
class AarDiffer extends AndroidArchiveDiffer {
|
||||
class AarDiffer extends ArchiveDiffer {
|
||||
String _hash(List<int> bytes) => sha256.convert(bytes).toString();
|
||||
|
||||
@override
|
||||
Set<String> changedFiles(String archivePath1, String archivePath2) =>
|
||||
_fileHashes(File(archivePath1))
|
||||
.difference(_fileHashes(File(archivePath2)))
|
||||
.map((pair) => pair.$1)
|
||||
.toSet();
|
||||
FileSetDiff changedFiles(String oldArchivePath, String newArchivePath) =>
|
||||
FileSetDiff.fromPathHashes(
|
||||
oldPathHashes: _fileHashes(File(oldArchivePath)),
|
||||
newPathHashes: _fileHashes(File(newArchivePath)),
|
||||
);
|
||||
|
||||
Set<(String, String)> _fileHashes(File aar) => ZipDecoder()
|
||||
.decodeBuffer(InputFileStream(aar.path))
|
||||
.files
|
||||
.where((file) => file.isFile)
|
||||
.map((file) => (file.name, _hash(file.content as List<int>)))
|
||||
.toSet();
|
||||
PathHashes _fileHashes(File aar) {
|
||||
final files = ZipDecoder()
|
||||
.decodeBuffer(InputFileStream(aar.path))
|
||||
.files
|
||||
.where((file) => file.isFile);
|
||||
return {
|
||||
for (final file in files) file.name: _hash(file.content as List<int>)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
|
||||
abstract class AndroidArchiveDiffer {
|
||||
/// Files that have changed between the archives at the two provided paths.
|
||||
Set<String> changedFiles(String archivePath1, String archivePath2);
|
||||
|
||||
/// Whether any changed files correspond to a change in assets.
|
||||
bool hasAssetChanges(Set<String> paths) {
|
||||
const assetDirNames = ['assets', 'res'];
|
||||
const assetFileNames = ['AssetManifest.json'];
|
||||
return paths.any(
|
||||
(path) =>
|
||||
p.split(path).any((component) => assetDirNames.contains(component)) ||
|
||||
assetFileNames.contains(p.basename(path)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether any changed files correspond to a change in Dart code.
|
||||
bool hasDartChanges(Set<String> paths) {
|
||||
const dartFileNames = ['libapp.so', 'libflutter.so'];
|
||||
return paths.any((path) => dartFileNames.contains(p.basename(path)));
|
||||
}
|
||||
|
||||
/// Whether changed files correspond to a change in Java or Kotlin code.
|
||||
bool hasNativeChanges(Set<String> path) {
|
||||
return path.any((path) => p.extension(path) == '.dex');
|
||||
}
|
||||
|
||||
/// The types of differences between the archives at the two provided paths.
|
||||
Set<ArchiveDifferences> contentDifferences(
|
||||
String archivePath1,
|
||||
String archivePath2,
|
||||
) {
|
||||
final changedFilePaths = changedFiles(archivePath1, archivePath2);
|
||||
final differences = <ArchiveDifferences>{};
|
||||
if (hasDartChanges(changedFilePaths)) {
|
||||
differences.add(ArchiveDifferences.dart);
|
||||
}
|
||||
if (hasAssetChanges(changedFilePaths)) {
|
||||
differences.add(ArchiveDifferences.assets);
|
||||
}
|
||||
if (hasNativeChanges(changedFilePaths)) {
|
||||
differences.add(ArchiveDifferences.native);
|
||||
}
|
||||
|
||||
return differences;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export 'aab_differ.dart';
|
||||
export 'aar_differ.dart';
|
||||
export 'archive_differences.dart';
|
||||
export 'file_set_diff.dart';
|
||||
export 'ipa.dart';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
|
||||
/// Computes content differences between two archives.
|
||||
abstract class ArchiveDiffer {
|
||||
/// Files that have been added, removed, or that have changed between the
|
||||
/// archives at the two provided paths.
|
||||
FileSetDiff changedFiles(String oldArchivePath, String newArchivePath);
|
||||
|
||||
/// Whether any changed files correspond to a change in assets.
|
||||
static Set<String> assetChanges(Set<String> paths) {
|
||||
const assetDirNames = ['assets', 'res'];
|
||||
const assetFileNames = ['AssetManifest.json'];
|
||||
return paths
|
||||
.where(
|
||||
(path) =>
|
||||
p
|
||||
.split(path)
|
||||
.any((component) => assetDirNames.contains(component)) ||
|
||||
assetFileNames.contains(p.basename(path)),
|
||||
)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
/// Whether any changed files correspond to a change in Dart code.
|
||||
static Set<String> dartChanges(Set<String> paths) {
|
||||
const dartFileNames = ['libapp.so', 'libflutter.so'];
|
||||
return paths
|
||||
.where((path) => dartFileNames.contains(p.basename(path)))
|
||||
.toSet();
|
||||
}
|
||||
|
||||
/// Whether changed files correspond to a change in native code.
|
||||
static Set<String> nativeChanges(Set<String> path) {
|
||||
// TODO(bryanoltman): add support for iOS native code changes.
|
||||
return path.where((path) => p.extension(path) == '.dex').toSet();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/// Types of code changes that we care about.
|
||||
enum ArchiveDifferences {
|
||||
dart,
|
||||
native,
|
||||
assets,
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
|
||||
/// Maps file paths to SHA-256 hash digests.
|
||||
typedef PathHashes = Map<String, String>;
|
||||
|
||||
/// Sets of [PathHashes] that represent changes between two sets of files.
|
||||
class FileSetDiff {
|
||||
FileSetDiff({
|
||||
required this.addedPaths,
|
||||
required this.removedPaths,
|
||||
required this.changedPaths,
|
||||
});
|
||||
|
||||
/// Creates a [FileSetDiff] showing added, changed, and removed file sets
|
||||
factory FileSetDiff.fromPathHashes({
|
||||
required PathHashes oldPathHashes,
|
||||
required PathHashes newPathHashes,
|
||||
}) {
|
||||
final oldPaths = oldPathHashes.keys.toSet();
|
||||
final newPaths = newPathHashes.keys.toSet();
|
||||
return FileSetDiff(
|
||||
addedPaths: newPaths.difference(oldPaths),
|
||||
removedPaths: oldPaths.difference(newPaths),
|
||||
changedPaths: oldPaths
|
||||
.intersection(newPaths)
|
||||
.where((name) => oldPathHashes[name] != newPathHashes[name])
|
||||
.toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
FileSetDiff.empty()
|
||||
: addedPaths = {},
|
||||
removedPaths = {},
|
||||
changedPaths = {};
|
||||
|
||||
/// File paths that were added.
|
||||
final Set<String> addedPaths;
|
||||
|
||||
/// File paths that were removed.
|
||||
final Set<String> removedPaths;
|
||||
|
||||
/// File paths that were changed.
|
||||
final Set<String> changedPaths;
|
||||
|
||||
/// Whether all path sets are empty.
|
||||
bool get isEmpty => !isNotEmpty;
|
||||
|
||||
/// Whether any files were added, changed, or removed.
|
||||
bool get isNotEmpty =>
|
||||
addedPaths.isNotEmpty ||
|
||||
removedPaths.isNotEmpty ||
|
||||
changedPaths.isNotEmpty;
|
||||
|
||||
/// A subset of this [FileSetDiff] that only contains paths that correspond
|
||||
/// to a change in Dart code.
|
||||
FileSetDiff get dartChanges => FileSetDiff(
|
||||
addedPaths: ArchiveDiffer.dartChanges(addedPaths),
|
||||
removedPaths: ArchiveDiffer.dartChanges(removedPaths),
|
||||
changedPaths: ArchiveDiffer.dartChanges(changedPaths),
|
||||
);
|
||||
|
||||
/// A subset of this [FileSetDiff] that only contains paths that correspond
|
||||
/// to changes in native code.
|
||||
FileSetDiff get nativeChanges => FileSetDiff(
|
||||
addedPaths: ArchiveDiffer.nativeChanges(addedPaths),
|
||||
removedPaths: ArchiveDiffer.nativeChanges(removedPaths),
|
||||
changedPaths: ArchiveDiffer.nativeChanges(changedPaths),
|
||||
);
|
||||
|
||||
/// A subset of this [FileSetDiff] that only contains paths that correspond
|
||||
/// to changes in bundled assets.
|
||||
FileSetDiff get assetChanges => FileSetDiff(
|
||||
addedPaths: ArchiveDiffer.assetChanges(addedPaths),
|
||||
removedPaths: ArchiveDiffer.assetChanges(removedPaths),
|
||||
changedPaths: ArchiveDiffer.assetChanges(changedPaths),
|
||||
);
|
||||
|
||||
/// A printable string representation of this [FileSetDiff].
|
||||
String get prettyString => [
|
||||
if (addedPaths.isNotEmpty)
|
||||
_prettyFileSetString(title: 'Added files', paths: addedPaths),
|
||||
if (changedPaths.isNotEmpty)
|
||||
_prettyFileSetString(title: 'Changed files', paths: changedPaths),
|
||||
if (removedPaths.isNotEmpty)
|
||||
_prettyFileSetString(title: 'Removed files', paths: removedPaths),
|
||||
].join('\n');
|
||||
|
||||
static String _prettyFileSetString({
|
||||
required String title,
|
||||
required Set<String> paths,
|
||||
}) {
|
||||
const padding = ' ';
|
||||
return '''
|
||||
$padding$title:
|
||||
${paths.sorted().map((p) => '${padding * 2}$p').join('\n')}''';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,31 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
|
||||
/// {@template mf_entry}
|
||||
/// A single entry from an .MF file.
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class MfEntry {
|
||||
/// {@macro mf_entry}
|
||||
const MfEntry({
|
||||
required this.name,
|
||||
required this.sha256Digest,
|
||||
});
|
||||
|
||||
/// Contents of the `Name` field.
|
||||
final String name;
|
||||
|
||||
/// Contents of the `SHA-256-Digest` field.
|
||||
final String sha256Digest;
|
||||
|
||||
@override
|
||||
String toString() => 'MfEntry(name: $name, sha256Digest: $sha256Digest)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MfEntry &&
|
||||
runtimeType == other.runtimeType &&
|
||||
name == other.name &&
|
||||
sha256Digest == other.sha256Digest;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([name, sha256Digest]);
|
||||
}
|
||||
|
||||
/// Parses a .MF file into a list of [MfEntry]s.
|
||||
/// Parses a .MF file into a [PathHashes] map.
|
||||
class MfReader {
|
||||
static final nameRegex = RegExp(r'^Name: (.+)$');
|
||||
static final nameContinuedRegex = RegExp(r'^ (.+)$');
|
||||
static final shaDigestRegex = RegExp(r'^SHA-256-Digest: (.+)$');
|
||||
|
||||
/// Parses the content of [mfFile] into a list of [MfEntry]s.
|
||||
/// Parses the content of [mfFile] into a [PathHashes] map.
|
||||
///
|
||||
/// [mfFile] should be a JAR manifest file, as described in
|
||||
/// https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html.
|
||||
static List<MfEntry> read(File mfFile) => parse(mfFile.readAsStringSync());
|
||||
static PathHashes read(File mfFile) => parse(mfFile.readAsStringSync());
|
||||
|
||||
/// Parses the contents [mfContents] file into a list of [MfEntry]s.
|
||||
/// Parses the contents [mfContents] file into a [PathHashes] map.
|
||||
///
|
||||
/// [mfContents] should be a JAR manifest file, as described in
|
||||
/// https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html.
|
||||
static List<MfEntry> parse(String mfContents) {
|
||||
static PathHashes parse(String mfContents) {
|
||||
final lines = mfContents.split('\n').map((line) => line.trimRight());
|
||||
final entries = <MfEntry>[];
|
||||
final entries = <String, String>{};
|
||||
var currentHash = '';
|
||||
var currentName = '';
|
||||
for (final line in lines) {
|
||||
if (line.isEmpty && currentName.isNotEmpty && currentHash.isNotEmpty) {
|
||||
entries.add(MfEntry(name: currentName, sha256Digest: currentHash));
|
||||
entries[currentName] = currentHash;
|
||||
currentHash = '';
|
||||
currentName = '';
|
||||
} else if (nameRegex.hasMatch(line)) {
|
||||
@@ -70,7 +38,7 @@ class MfReader {
|
||||
}
|
||||
|
||||
if (currentName.isNotEmpty && currentHash.isNotEmpty) {
|
||||
entries.add(MfEntry(name: currentName, sha256Digest: currentHash));
|
||||
entries[currentName] = currentHash;
|
||||
}
|
||||
|
||||
return entries;
|
||||
|
||||
@@ -223,7 +223,7 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
final aarDiffProgress =
|
||||
logger.progress('Checking for aar content differences');
|
||||
|
||||
final contentDiffs = _aarDiffer.contentDifferences(
|
||||
final contentDiffs = _aarDiffer.changedFiles(
|
||||
releaseAarPath,
|
||||
aarArtifactPath(
|
||||
packageName: androidPackageName!,
|
||||
@@ -233,7 +233,7 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
|
||||
aarDiffProgress.complete();
|
||||
|
||||
if (contentDiffs.contains(ArchiveDifferences.assets)) {
|
||||
if (contentDiffs.assetChanges.isNotEmpty) {
|
||||
logger.info(
|
||||
yellow.wrap(
|
||||
'''⚠️ The Android Archive contains asset changes, which will not be included in the patch.''',
|
||||
|
||||
@@ -205,8 +205,7 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
platform: platformName,
|
||||
);
|
||||
|
||||
final releaseAabArtifact =
|
||||
await codePushClientWrapper.maybeGetReleaseArtifact(
|
||||
final releaseAabArtifact = await codePushClientWrapper.getReleaseArtifact(
|
||||
releaseId: release.id,
|
||||
arch: 'aab',
|
||||
platform: platformName,
|
||||
@@ -229,14 +228,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
}
|
||||
}
|
||||
|
||||
String? releaseAabPath;
|
||||
String releaseAabPath;
|
||||
try {
|
||||
if (releaseAabArtifact != null) {
|
||||
releaseAabPath = await downloadReleaseArtifact(
|
||||
Uri.parse(releaseAabArtifact.url),
|
||||
httpClient: _httpClient,
|
||||
);
|
||||
}
|
||||
releaseAabPath = await downloadReleaseArtifact(
|
||||
Uri.parse(releaseAabArtifact.url),
|
||||
httpClient: _httpClient,
|
||||
);
|
||||
} catch (error) {
|
||||
downloadReleaseArtifactProgress.fail('$error');
|
||||
return ExitCode.software.code;
|
||||
@@ -244,20 +241,19 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
|
||||
downloadReleaseArtifactProgress.complete();
|
||||
|
||||
final contentDiffs = releaseAabPath == null
|
||||
? <ArchiveDifferences>{}
|
||||
: _aabDiffer.contentDifferences(
|
||||
releaseAabPath,
|
||||
bundlePath,
|
||||
);
|
||||
final contentDiffs = _aabDiffer.changedFiles(
|
||||
releaseAabPath,
|
||||
bundlePath,
|
||||
);
|
||||
|
||||
logger.detail('aab content differences: $contentDiffs');
|
||||
|
||||
if (contentDiffs.contains(ArchiveDifferences.native)) {
|
||||
if (contentDiffs.nativeChanges.isNotEmpty) {
|
||||
logger
|
||||
..err(
|
||||
'''The Android App Bundle appears to contain Kotlin or Java changes, which cannot be applied via a patch.''',
|
||||
)
|
||||
..info(yellow.wrap(contentDiffs.nativeChanges.prettyString))
|
||||
..info(
|
||||
yellow.wrap(
|
||||
'''
|
||||
@@ -269,12 +265,12 @@ If you believe you're seeing this in error, please reach out to us for support a
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
if (contentDiffs.contains(ArchiveDifferences.assets)) {
|
||||
logger.info(
|
||||
yellow.wrap(
|
||||
'''⚠️ The Android App Bundle contains asset changes, which will not be included in the patch.''',
|
||||
),
|
||||
);
|
||||
if (contentDiffs.assetChanges.isNotEmpty) {
|
||||
logger
|
||||
..warn(
|
||||
'''The Android App Bundle contains asset changes, which will not be included in the patch.''',
|
||||
)
|
||||
..info(yellow.wrap(contentDiffs.assetChanges.prettyString));
|
||||
final shouldContinue = logger.confirm('Continue anyways?');
|
||||
if (!shouldContinue) {
|
||||
return ExitCode.success.code;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -20,14 +20,14 @@ void main() {
|
||||
differ = AabDiffer();
|
||||
});
|
||||
|
||||
group('changedFiles', () {
|
||||
group('changedPaths', () {
|
||||
test('finds no differences between the same aab', () {
|
||||
expect(differ.changedFiles(baseAabPath, baseAabPath), isEmpty);
|
||||
});
|
||||
|
||||
test('finds differences between two different aabs', () {
|
||||
expect(
|
||||
differ.changedFiles(baseAabPath, changedDartAabPath).toSet(),
|
||||
differ.changedFiles(baseAabPath, changedDartAabPath).changedPaths,
|
||||
{
|
||||
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb',
|
||||
'base/lib/arm64-v8a/libapp.so',
|
||||
@@ -40,38 +40,39 @@ void main() {
|
||||
|
||||
group('contentDifferences', () {
|
||||
test('detects no differences between the same aab', () {
|
||||
expect(differ.contentDifferences(baseAabPath, baseAabPath), isEmpty);
|
||||
expect(differ.changedFiles(baseAabPath, baseAabPath), isEmpty);
|
||||
});
|
||||
|
||||
test('detects asset changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAabPath, changedAssetAabPath),
|
||||
{ArchiveDifferences.assets},
|
||||
);
|
||||
final fileSetDiff =
|
||||
differ.changedFiles(baseAabPath, changedAssetAabPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isTrue);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('detects kotlin changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAabPath, changedKotlinAabPath),
|
||||
{ArchiveDifferences.native},
|
||||
);
|
||||
final fileSetDiff =
|
||||
differ.changedFiles(baseAabPath, changedKotlinAabPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isTrue);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isTrue);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isFalse);
|
||||
});
|
||||
|
||||
test('detects dart changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAabPath, changedDartAabPath),
|
||||
{ArchiveDifferences.dart},
|
||||
);
|
||||
final fileSetDiff =
|
||||
differ.changedFiles(baseAabPath, changedDartAabPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isTrue);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('detects dart and asset changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAabPath, changedDartAndAssetAabPath),
|
||||
{
|
||||
ArchiveDifferences.assets,
|
||||
ArchiveDifferences.dart,
|
||||
},
|
||||
);
|
||||
final fileSetDiff =
|
||||
differ.changedFiles(baseAabPath, changedDartAndAssetAabPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,46 +16,44 @@ void main() {
|
||||
differ = AarDiffer();
|
||||
});
|
||||
|
||||
group('changedFiles', () {
|
||||
group('changedPaths', () {
|
||||
test('finds no differences between the same aar', () {
|
||||
expect(differ.changedFiles(baseAarPath, baseAarPath), isEmpty);
|
||||
});
|
||||
|
||||
test('finds differences between two different aars', () {
|
||||
expect(
|
||||
differ.changedFiles(baseAarPath, changedDartAarPath).toSet(),
|
||||
differ.changedFiles(baseAarPath, changedDartAarPath).changedPaths,
|
||||
{'jni/arm64-v8a/libapp.so'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('contentDifferences', () {
|
||||
group('changedFiles', () {
|
||||
test('detects no differences between the same aar', () {
|
||||
expect(differ.contentDifferences(baseAarPath, baseAarPath), isEmpty);
|
||||
expect(differ.changedFiles(baseAarPath, baseAarPath), isEmpty);
|
||||
});
|
||||
|
||||
test('detects asset changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAarPath, changedAssetAarPath),
|
||||
{ArchiveDifferences.assets},
|
||||
);
|
||||
final fileSetDiff = differ.changedFiles(baseAarPath, changedAssetAarPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isTrue);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('detects dart changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAarPath, changedDartAarPath),
|
||||
{ArchiveDifferences.dart},
|
||||
);
|
||||
final fileSetDiff = differ.changedFiles(baseAarPath, changedDartAarPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isTrue);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('detects dart and asset changes', () {
|
||||
expect(
|
||||
differ.contentDifferences(baseAarPath, changedDartAndAssetAarPath),
|
||||
{
|
||||
ArchiveDifferences.assets,
|
||||
ArchiveDifferences.dart,
|
||||
},
|
||||
);
|
||||
final fileSetDiff =
|
||||
differ.changedFiles(baseAarPath, changedDartAndAssetAarPath);
|
||||
expect(fileSetDiff.assetChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.dartChanges.isEmpty, isFalse);
|
||||
expect(fileSetDiff.nativeChanges.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group(FileSetDiff, () {
|
||||
group('fromPathHashes', () {
|
||||
test('detects added, changed, and removed files', () {
|
||||
final oldPathHashes = {
|
||||
'a': 'asdf',
|
||||
'b': 'qwer',
|
||||
};
|
||||
final newPathHashes = {
|
||||
'a': 'qwer',
|
||||
'c': 'zxcv',
|
||||
};
|
||||
|
||||
final fileSetDiff = FileSetDiff.fromPathHashes(
|
||||
oldPathHashes: oldPathHashes,
|
||||
newPathHashes: newPathHashes,
|
||||
);
|
||||
expect(fileSetDiff.addedPaths, {'c'});
|
||||
expect(fileSetDiff.changedPaths, {'a'});
|
||||
expect(fileSetDiff.removedPaths, {'b'});
|
||||
});
|
||||
});
|
||||
|
||||
group('prettyString', () {
|
||||
test('returns a string with added, changed, and removed files', () {
|
||||
final fileSetDiff = FileSetDiff(
|
||||
addedPaths: {'a', 'b'},
|
||||
changedPaths: {'c'},
|
||||
removedPaths: {'d'},
|
||||
);
|
||||
expect(
|
||||
fileSetDiff.prettyString,
|
||||
'''
|
||||
Added files:
|
||||
a
|
||||
b
|
||||
Changed files:
|
||||
c
|
||||
Removed files:
|
||||
d''',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not include empty path sets', () {
|
||||
final fileSetDiff = FileSetDiff(
|
||||
addedPaths: {'a', 'b'},
|
||||
changedPaths: {},
|
||||
removedPaths: {},
|
||||
);
|
||||
expect(
|
||||
fileSetDiff.prettyString,
|
||||
'''
|
||||
Added files:
|
||||
a
|
||||
b''',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('isEmpty is true if all path sets are empty', () {
|
||||
final fileSetDiff = FileSetDiff.empty();
|
||||
expect(fileSetDiff.isEmpty, isTrue);
|
||||
expect(fileSetDiff.isNotEmpty, isFalse);
|
||||
});
|
||||
|
||||
test('isEmpty is false if any path sets are not empty', () {
|
||||
final fileSetDiff = FileSetDiff(
|
||||
addedPaths: {'a'},
|
||||
changedPaths: {},
|
||||
removedPaths: {},
|
||||
);
|
||||
expect(fileSetDiff.isEmpty, isFalse);
|
||||
expect(fileSetDiff.isNotEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,13 +5,6 @@ import 'package:shorebird_cli/src/archive_analysis/mf_reader.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group(MfEntry, () {
|
||||
test('toString contains name and sha', () {
|
||||
const entry = MfEntry(name: 'name', sha256Digest: '1234abcd');
|
||||
expect(entry.toString(), 'MfEntry(name: name, sha256Digest: 1234abcd)');
|
||||
});
|
||||
});
|
||||
|
||||
group(MfReader, () {
|
||||
const fileContent = '''
|
||||
Manifest-Version: 1.0
|
||||
@@ -35,35 +28,21 @@ SHA-256-Digest: WkS2WF7aQZuQ91vLhCXq0vXjRx702kn5K6nAYwQiDec=
|
||||
Name: base/dex/classes.dex
|
||||
SHA-256-Digest: wCxl8B3GnKZdBjEOIaW/AhLmIQYXIlYwVC9VRisYGsw=''';
|
||||
|
||||
final expectedEntries = {
|
||||
const MfEntry(
|
||||
name:
|
||||
'BUNDLE-METADATA/com.android.tools.build.gradle/app-metadata.properties',
|
||||
sha256Digest: 'Y9a0mKIrJP9ygajC+nXOu9HRrHGFilTiRYHA5x3cZRs=',
|
||||
),
|
||||
const MfEntry(
|
||||
name:
|
||||
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb',
|
||||
sha256Digest: 'b23DKN21/V4M5TUGFR9G37D30i886zSRJ93jFr8hEfQ=',
|
||||
),
|
||||
const MfEntry(
|
||||
name:
|
||||
'BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map',
|
||||
sha256Digest: 'pJO/g5sJghLBCx1iv55JLFRgrYHQF9blCp3j/sIpv/s=',
|
||||
),
|
||||
const MfEntry(
|
||||
name: 'base/assets/flutter_assets/shorebird.yaml',
|
||||
sha256Digest: 'WkS2WF7aQZuQ91vLhCXq0vXjRx702kn5K6nAYwQiDec=',
|
||||
),
|
||||
const MfEntry(
|
||||
name: 'base/dex/classes.dex',
|
||||
sha256Digest: 'wCxl8B3GnKZdBjEOIaW/AhLmIQYXIlYwVC9VRisYGsw=',
|
||||
),
|
||||
final expectedPathHashes = {
|
||||
'BUNDLE-METADATA/com.android.tools.build.gradle/app-metadata.properties':
|
||||
'Y9a0mKIrJP9ygajC+nXOu9HRrHGFilTiRYHA5x3cZRs=',
|
||||
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb':
|
||||
'b23DKN21/V4M5TUGFR9G37D30i886zSRJ93jFr8hEfQ=',
|
||||
'BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map':
|
||||
'pJO/g5sJghLBCx1iv55JLFRgrYHQF9blCp3j/sIpv/s=',
|
||||
'base/assets/flutter_assets/shorebird.yaml':
|
||||
'WkS2WF7aQZuQ91vLhCXq0vXjRx702kn5K6nAYwQiDec=',
|
||||
'base/dex/classes.dex': 'wCxl8B3GnKZdBjEOIaW/AhLmIQYXIlYwVC9VRisYGsw=',
|
||||
};
|
||||
|
||||
test('parses content string into MfEntry list', () {
|
||||
final entries = MfReader.parse(fileContent);
|
||||
expect(entries.toSet(), expectedEntries);
|
||||
expect(entries, expectedPathHashes);
|
||||
});
|
||||
|
||||
test('parses file contents into MfEntry list', () {
|
||||
@@ -71,7 +50,7 @@ SHA-256-Digest: wCxl8B3GnKZdBjEOIaW/AhLmIQYXIlYwVC9VRisYGsw=''';
|
||||
final file = File(p.join(tempDir.path, 'MANIFEST.MF'))
|
||||
..writeAsStringSync(fileContent);
|
||||
final entries = MfReader.read(file);
|
||||
expect(entries.toSet(), expectedEntries);
|
||||
expect(entries, expectedPathHashes);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,7 +246,8 @@ flutter:
|
||||
return patchProcessResult;
|
||||
});
|
||||
|
||||
when(() => aarDiffer.contentDifferences(any(), any())).thenReturn({});
|
||||
when(() => aarDiffer.changedFiles(any(), any()))
|
||||
.thenReturn(FileSetDiff.empty());
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => argResults['channel']).thenReturn(channelName);
|
||||
when(() => argResults['dry-run']).thenReturn(false);
|
||||
@@ -517,8 +518,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
});
|
||||
|
||||
test('prompts user to continue when asset changes are detected', () async {
|
||||
when(() => aarDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.assets},
|
||||
when(() => aarDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {'assets/test.json'},
|
||||
removedPaths: {},
|
||||
changedPaths: {},
|
||||
),
|
||||
);
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
@@ -544,8 +549,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
test(
|
||||
'''does not warn user of asset or code changes if only dart changes are detected''',
|
||||
() async {
|
||||
when(() => aarDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.dart},
|
||||
when(() => aarDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {},
|
||||
removedPaths: {},
|
||||
changedPaths: {'some/path/libapp.so'},
|
||||
),
|
||||
);
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
@@ -571,8 +580,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
test(
|
||||
'''exits if user decides to not proceed after being warned of non-dart changes''',
|
||||
() async {
|
||||
when(() => aarDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.assets},
|
||||
when(() => aarDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {'assets/test.json'},
|
||||
removedPaths: {},
|
||||
changedPaths: {},
|
||||
),
|
||||
);
|
||||
when(
|
||||
() => logger.confirm(any(that: contains('Continue anyways?'))),
|
||||
|
||||
@@ -247,7 +247,8 @@ flutter:
|
||||
: releaseVersionNameProcessResult;
|
||||
});
|
||||
|
||||
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn({});
|
||||
when(() => aabDiffer.changedFiles(any(), any()))
|
||||
.thenReturn(FileSetDiff.empty());
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => argResults['arch']).thenReturn(arch);
|
||||
when(() => argResults['channel']).thenReturn(channelName);
|
||||
@@ -311,7 +312,7 @@ flutter:
|
||||
},
|
||||
);
|
||||
when(
|
||||
() => codePushClientWrapper.maybeGetReleaseArtifact(
|
||||
() => codePushClientWrapper.getReleaseArtifact(
|
||||
releaseId: any(named: 'releaseId'),
|
||||
arch: 'aab',
|
||||
platform: 'android',
|
||||
@@ -558,8 +559,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
});
|
||||
|
||||
test('throws error when Java/Kotlin code changes are detected', () async {
|
||||
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.native},
|
||||
when(() => aabDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {},
|
||||
removedPaths: {},
|
||||
changedPaths: {'some/path/to/changed.dex'},
|
||||
),
|
||||
);
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
@@ -578,8 +583,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
});
|
||||
|
||||
test('prompts user to continue when asset changes are detected', () async {
|
||||
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.assets},
|
||||
when(() => aabDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {},
|
||||
removedPaths: {},
|
||||
changedPaths: {'assets/test.json'},
|
||||
),
|
||||
);
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
@@ -591,7 +600,7 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
|
||||
expect(exitCode, ExitCode.success.code);
|
||||
verify(
|
||||
() => logger.info(
|
||||
() => logger.warn(
|
||||
any(
|
||||
that: contains(
|
||||
'''The Android App Bundle contains asset changes, which will not be included in the patch.''',
|
||||
@@ -605,8 +614,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
test(
|
||||
'''does not warn user of asset or code changes if only dart changes are detected''',
|
||||
() async {
|
||||
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.dart},
|
||||
when(() => aabDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {},
|
||||
removedPaths: {},
|
||||
changedPaths: {'some/path/to/libapp.so'},
|
||||
),
|
||||
);
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
@@ -637,8 +650,12 @@ https://github.com/shorebirdtech/shorebird/issues/472
|
||||
test(
|
||||
'''exits if user decides to not proceed after being warned of non-dart changes''',
|
||||
() async {
|
||||
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
|
||||
{ArchiveDifferences.assets},
|
||||
when(() => aabDiffer.changedFiles(any(), any())).thenReturn(
|
||||
FileSetDiff(
|
||||
addedPaths: {},
|
||||
removedPaths: {},
|
||||
changedPaths: {'assets/test.json'},
|
||||
),
|
||||
);
|
||||
when(
|
||||
() => logger.confirm(any(that: contains('Continue anyways?'))),
|
||||
|
||||
Reference in New Issue
Block a user