From c50b3fea32b61b86dd0c34b3677a05a02e3db62b Mon Sep 17 00:00:00 2001 From: Tess Strickland Date: Mon, 16 Sep 2024 19:55:05 +0200 Subject: [PATCH] Extend 'flutter symbolize' to handle deferred loading units. (#149315) Adds `-u`/`--unit-id-debug-info` arguments to `flutter symbolize` to pass paths to DWARF information for deferred loading units. The argument passed via `-u` should be of the form `N:P`, where `N` is the loading unit ID (an integer) and `P` is the path to the debug information for loading unit `N`. The DWARF information for the root loading unit can either be passed by `-d`/`--debug-info` as before or by `--unit-id-debug-info 1:`. Partial fix for https://github.com/flutter/flutter/issues/137527. Additional work is needed to adjust tools built on top of `flutter symbolize` to store and pass along this additional information appropriately when there are deferred loading units. --- .../lib/src/commands/symbolize.dart | 208 ++++++++--- .../hermetic/symbolize_test.dart | 341 +++++++++++++++++- 2 files changed, 478 insertions(+), 71 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/symbolize.dart b/packages/flutter_tools/lib/src/commands/symbolize.dart index 48091f73aa..63d6172efa 100644 --- a/packages/flutter_tools/lib/src/commands/symbolize.dart +++ b/packages/flutter_tools/lib/src/commands/symbolize.dart @@ -14,6 +14,8 @@ import '../base/io.dart'; import '../convert.dart'; import '../runner/flutter_command.dart'; +const int rootLoadingUnitId = 1; + /// Support for symbolizing a Dart stack trace. /// /// This command accepts either paths to an input file containing the @@ -34,6 +36,13 @@ class SymbolizeCommand extends FlutterCommand { valueHelp: '/out/android/app.arm64.symbols', help: 'A path to the symbols file generated with "--split-debug-info".' ); + argParser.addMultiOption( + 'unit-id-debug-info', + abbr: 'u', + valueHelp: '2:/out/android/app.arm64.symbols-2.part.so', + help: 'A loading unit id and the path to the symbols file for that' + ' unit generated with "--split-debug-info".' + ); argParser.addOption( 'input', abbr: 'i', @@ -63,18 +72,83 @@ class SymbolizeCommand extends FlutterCommand { @override bool get shouldUpdateCache => false; + File _handleDSYM(String fileName) { + final FileSystemEntityType type = _fileSystem.typeSync(fileName); + final bool isDSYM = fileName.endsWith('.dSYM'); + if (type == FileSystemEntityType.notFound) { + throw FileNotFoundException(fileName); + } + if (type == FileSystemEntityType.directory) { + if (!isDSYM) { + throw StateError('$fileName is a directory, not a file'); + } + final Directory dwarfDir = _fileSystem + .directory(fileName) + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF'); + // The DWARF directory inside the .dSYM contains a single MachO file. + return dwarfDir.listSync().single as File; + } + if (isDSYM) { + throw StateError('$fileName is not a dSYM package directory'); + } + return _fileSystem.file(fileName); + } + + Map _unitDebugInfoPathMap() { + final Map map = {}; + final String? rootInfo = stringArg('debug-info'); + if (rootInfo != null) { + map[rootLoadingUnitId] = _handleDSYM(rootInfo); + } + for (final String arg in stringsArg('unit-id-debug-info')) { + final int separatorIndex = arg.indexOf(':'); + final String unitIdString = arg.substring(0, separatorIndex); + final int unitId = int.parse(unitIdString); + final String unitDebugPath = arg.substring(separatorIndex + 1); + if (map.containsKey(unitId) && map[unitId]!.path != unitDebugPath) { + throw StateError('Different paths were given for the same loading unit' + ' $unitId: "${map[unitId]!.path}" and "$unitDebugPath".'); + } + map[unitId] = _handleDSYM(unitDebugPath); + } + return map; + } + @override - Future validateCommand() { - if (argResults?.wasParsed('debug-info') != true) { - throwToolExit('"--debug-info" is required to symbolize stack traces.'); + Future validateCommand() async { + if (argResults?.wasParsed('debug-info') != true && + argResults?.wasParsed('unit-id-debug-info') != true) { + throwToolExit( + 'Either "--debug-info" or "--unit-id-debug-info" is required to symbolize stack traces.'); } - final String debugInfoPath = stringArg('debug-info')!; - if (debugInfoPath.endsWith('.dSYM') - ? !_fileSystem.isDirectorySync(debugInfoPath) - : !_fileSystem.isFileSync(debugInfoPath)) { - throwToolExit('$debugInfoPath does not exist.'); + for (final String arg in stringsArg('unit-id-debug-info')) { + final int separatorIndex = arg.indexOf(':'); + if (separatorIndex == -1) { + throwToolExit( + 'The argument to "--unit-id-debug-info" must contain a unit ID and path,' + ' separated by ":": "$arg".'); + } + final String unitIdString = arg.substring(0, separatorIndex); + final int? unitId = int.tryParse(unitIdString); + if (unitId == null) { + throwToolExit('The argument to "--unit-id-debug-info" must begin with' + ' a unit ID: "$unitIdString" is not an integer.'); + } } - if ((argResults?.wasParsed('input') ?? false) && !_fileSystem.isFileSync(stringArg('input')!)) { + late final Map map; + try { + map = _unitDebugInfoPathMap(); + } on Object catch (e) { + throwToolExit(e.toString()); + } + if (!map.containsKey(rootLoadingUnitId)) { + throwToolExit('Missing debug info for the root loading unit' + ' (id $rootLoadingUnitId).'); + } + if ((argResults?.wasParsed('input') ?? false) && + !await _fileSystem.isFile(stringArg('input')!)) { throwToolExit('${stringArg('input')} does not exist.'); } return super.validateCommand(); @@ -82,10 +156,8 @@ class SymbolizeCommand extends FlutterCommand { @override Future runCommand() async { - Stream> input; - IOSink output; - // Configure output to either specified file or stdout. + late final IOSink output; if (argResults?.wasParsed('output') ?? false) { final File outputFile = _fileSystem.file(stringArg('output')); if (!outputFile.parent.existsSync()) { @@ -93,44 +165,28 @@ class SymbolizeCommand extends FlutterCommand { } output = outputFile.openWrite(); } else { - final StreamController> outputController = StreamController>(); - outputController - .stream - .transform(utf8.decoder) - .listen(_stdio.stdoutWrite); + final StreamController> outputController = + StreamController>(); + outputController.stream + .transform(utf8.decoder) + .listen(_stdio.stdoutWrite); output = IOSink(outputController); } // Configure input from either specified file or stdin. - if (argResults?.wasParsed('input') ?? false) { - input = _fileSystem.file(stringArg('input')).openRead(); - } else { - input = _stdio.stdin; - } + final Stream> input = (argResults?.wasParsed('input') ?? false) + ? _fileSystem.file(stringArg('input')).openRead() + : _stdio.stdin; - String debugInfoPath = stringArg('debug-info')!; + final Map unitSymbols = { + for (final MapEntry entry in _unitDebugInfoPathMap().entries) + entry.key: entry.value.readAsBytesSync(), + }; - // If it's a dSYM container, expand the path to the actual DWARF. - if (debugInfoPath.endsWith('.dSYM')) { - final Directory debugInfoDir = _fileSystem - .directory(debugInfoPath) - .childDirectory('Contents') - .childDirectory('Resources') - .childDirectory('DWARF'); - - final List dwarfFiles = debugInfoDir.listSync().whereType().toList(); - if (dwarfFiles.length == 1) { - debugInfoPath = dwarfFiles.first.path; - } else { - throwToolExit('Expected a single DWARF file in a dSYM container.'); - } - } - - final Uint8List symbols = _fileSystem.file(debugInfoPath).readAsBytesSync(); - await _dwarfSymbolizationService.decode( + await _dwarfSymbolizationService.decodeWithUnits( input: input, output: output, - symbols: symbols, + unitSymbols: unitSymbols, ); return FlutterCommandResult.success(); @@ -138,17 +194,34 @@ class SymbolizeCommand extends FlutterCommand { } typedef SymbolsTransformer = StreamTransformer Function(Uint8List); +typedef UnitSymbolsTransformer = StreamTransformer Function(Map); StreamTransformer _defaultTransformer(Uint8List symbols) { - final Dwarf? dwarf = Dwarf.fromBytes(symbols); - if (dwarf == null) { - throwToolExit('Failed to decode symbols file'); + return _defaultUnitsTransformer({ rootLoadingUnitId: symbols}); +} + +StreamTransformer _defaultUnitsTransformer(Map unitSymbols) { + final Map map = {}; + for (final int unitId in unitSymbols.keys) { + final Uint8List symbols = unitSymbols[unitId]!; + final Dwarf? dwarf = Dwarf.fromBytes(symbols); + if (dwarf == null) { + throwToolExit('Failed to decode symbols file for loading unit $unitId'); + } + map[unitId] = dwarf; } - return DwarfStackTraceDecoder(dwarf, includeInternalFrames: true); + if (!map.containsKey(rootLoadingUnitId)) { + throwToolExit('Missing symbols file for root loading unit (id $rootLoadingUnitId)'); + } + return DwarfStackTraceDecoder( + map[rootLoadingUnitId]!, + includeInternalFrames: true, + dwarfByUnitId: map, + ); } // A no-op transformer for `DwarfSymbolizationService.test` -StreamTransformer _testTransformer(Uint8List buffer) { +StreamTransformer _testUnitsTransformer(Map buffer) { return StreamTransformer.fromHandlers( handleData: (String data, EventSink sink) { sink.add(data); @@ -166,17 +239,24 @@ StreamTransformer _testTransformer(Uint8List buffer) { class DwarfSymbolizationService { const DwarfSymbolizationService({ SymbolsTransformer symbolsTransformer = _defaultTransformer, - }) : _transformer = symbolsTransformer; + }) : _transformer = symbolsTransformer, + _unitsTransformer = _defaultUnitsTransformer; + + const DwarfSymbolizationService.withUnits({ + UnitSymbolsTransformer unitSymbolsTransformer = _defaultUnitsTransformer, + }) : _transformer = null, + _unitsTransformer = unitSymbolsTransformer; /// Create a DwarfSymbolizationService with a no-op transformer for testing. @visibleForTesting factory DwarfSymbolizationService.test() { - return const DwarfSymbolizationService( - symbolsTransformer: _testTransformer + return const DwarfSymbolizationService.withUnits( + unitSymbolsTransformer: _testUnitsTransformer, ); } - final SymbolsTransformer _transformer; + final SymbolsTransformer? _transformer; + final UnitSymbolsTransformer _unitsTransformer; /// Decode a stack trace from [input] and place the results in [output]. /// @@ -190,13 +270,37 @@ class DwarfSymbolizationService { required IOSink output, required Uint8List symbols, }) async { + await decodeWithUnits( + input: input, + output: output, + unitSymbols: { + rootLoadingUnitId: symbols, + }, + ); + } + + /// Decode a stack trace from [input] and place the results in [output]. + /// + /// Requires [unitSymbols] to map integer unit IDs to buffers created from + /// the `--split-debug-info` command line flag. + /// + /// Throws a [ToolExit] if the symbols cannot be parsed or the stack trace + /// cannot be decoded. + Future decodeWithUnits({ + required Stream> input, + required IOSink output, + required Map unitSymbols, + }) async { + final UnitSymbolsTransformer unitSymbolsTransformer = _transformer != null + ? ((Map m) => _transformer(m[rootLoadingUnitId]!)) + : _unitsTransformer; final Completer onDone = Completer(); StreamSubscription? subscription; subscription = input .cast>() .transform(const Utf8Decoder()) .transform(const LineSplitter()) - .transform(_transformer(symbols)) + .transform(unitSymbolsTransformer(unitSymbols)) .listen((String line) { try { output.writeln(line); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart index 861e1a6a88..731f67180b 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart @@ -52,7 +52,7 @@ void main() { }); - testUsingContext('symbolize exits when --debug-info argument is missing', () async { + testUsingContext('symbolize exits when --debug-info and --unit-id-debug-info arguments are missing', () async { final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, @@ -61,85 +61,388 @@ void main() { final Future result = createTestCommandRunner(command) .run(const ['symbolize']); - expect(result, throwsToolExit(message: '"--debug-info" is required to symbolize stack traces.')); + expect(result, throwsToolExit(message: 'Either "--debug-info" or "--unit-id-debug-info" is required to symbolize stack traces.')); }, overrides: { OutputPreferences: () => OutputPreferences.test(), }); testUsingContext('symbolize exits when --debug-info dwarf file is missing', () async { + const String fileName = 'app.debug'; final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, dwarfSymbolizationService: DwarfSymbolizationService.test(), ); final Future result = createTestCommandRunner(command) - .run(const ['symbolize', '--debug-info=app.debug']); + .run(const ['symbolize', '--debug-info=$fileName']); - expect(result, throwsToolExit(message: 'app.debug does not exist.')); + expect(result, throwsToolExit(message: 'File not found: $fileName')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits when --unit-id-debug-info dwarf file is missing', () async { + const String fileName = 'app.debug'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$fileName']); + + expect(result, throwsToolExit(message: 'File not found: $fileName')); }, overrides: { OutputPreferences: () => OutputPreferences.test(), }); testUsingContext('symbolize exits when --debug-info dSYM is missing', () async { + const String fileName = 'app.dSYM'; final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, dwarfSymbolizationService: DwarfSymbolizationService.test(), ); final Future result = createTestCommandRunner(command) - .run(const ['symbolize', '--debug-info=app.dSYM']); + .run(const ['symbolize', '--debug-info=$fileName']); - expect(result, throwsToolExit(message: 'app.dSYM does not exist.')); + expect(result, throwsToolExit(message: 'File not found: $fileName')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits when --unit-id-debug-info dSYM is missing', () async { + const String fileName = 'app.dSYM'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$fileName']); + + expect(result, throwsToolExit(message: 'File not found: $fileName')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits when --debug-info dSYM is not a directory', () async { + const String fileName = 'app.dSYM'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(fileName).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--debug-info=$fileName']); + + expect(result, throwsToolExit(message: '$fileName is not a dSYM package directory')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits when --unit-id-debug-info dSYM is not a directory', () async { + const String fileName = 'app.dSYM'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(fileName).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$fileName']); + + expect(result, throwsToolExit(message: '$fileName is not a dSYM package directory')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits if --unit-id-debug-info is just given a path', () async { + const String fileName = 'app.debug'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(fileName).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=$fileName']); + + expect(result, throwsToolExit(message: 'The argument to "--unit-id-debug-info" must contain a unit ID and path,' + ' separated by ":": "$fileName".')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits if the unit id for --unit-id-debug-info is not a valid integer', () async { + const String fileName = 'app.debug'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(fileName).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=foo:$fileName']); + + expect(result, throwsToolExit(message: 'The argument to "--unit-id-debug-info" must begin with' + ' a unit ID: "foo" is not an integer.')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits when different paths are given for the root loading unit via --debug-info and --unit-id-debug-info', () async { + const String fileName1 = 'app.debug'; + const String fileName2 = 'app2.debug'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(fileName1).createSync(); + fileSystem.file(fileName2).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--debug-info=$fileName1', '--unit-id-debug-info=$rootLoadingUnitId:$fileName2']); + + expect(result, throwsToolExit(message: 'Different paths were given for' + ' the same loading unit $rootLoadingUnitId: "$fileName1" and' + ' "$fileName2".')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize exits when different paths are given for a non-root loading unit via --unit-id-debug-info', () async { + const String fileName1 = 'app.debug'; + const String fileName2 = 'app2.debug'; + const String fileName3 = 'app3.debug'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(fileName1).createSync(); + fileSystem.file(fileName2).createSync(); + fileSystem.file(fileName3).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--debug-info=$fileName1', '--unit-id-debug-info=${rootLoadingUnitId+1}:$fileName2', '--unit-id-debug-info=${rootLoadingUnitId+1}:$fileName3']); + + expect(result, throwsToolExit(message: 'Different paths were given for' + ' the same loading unit ${rootLoadingUnitId+1}: "$fileName2" and' + ' "$fileName3".')); }, overrides: { OutputPreferences: () => OutputPreferences.test(), }); testUsingContext('symbolize exits when --input file is missing', () async { + const String fileName = 'app.debug'; final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, dwarfSymbolizationService: DwarfSymbolizationService.test(), ); - fileSystem.file('app.debug').createSync(); + fileSystem.file(fileName).createSync(); final Future result = createTestCommandRunner(command) - .run(const ['symbolize', '--debug-info=app.debug', '--input=foo.stack', '--output=results/foo.result']); + .run(const ['symbolize', '--debug-info=$fileName', '--input=foo.stack', '--output=results/foo.result']); expect(result, throwsToolExit(message: '')); }, overrides: { OutputPreferences: () => OutputPreferences.test(), }); - testUsingContext('symbolize succeeds when DwarfSymbolizationService does not throw', () async { + testUsingContext('symbolize exits when --debug-info argument is missing and --unit-id-debug-info is not provided for the root loading unit', () async { + const String fileName = 'app.debug'; final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, dwarfSymbolizationService: DwarfSymbolizationService.test(), ); - fileSystem.file('app.debug').writeAsBytesSync([1, 2, 3]); - fileSystem.file('foo.stack').writeAsStringSync('hello'); + fileSystem.file(fileName).createSync(); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=${rootLoadingUnitId+1}:$fileName']); + + expect(result, throwsToolExit(message: 'Missing debug info for the root loading unit (id $rootLoadingUnitId).')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize succeeds when DwarfSymbolizationService does not throw', () async { + const String debugName = 'app.debug'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); await createTestCommandRunner(command) - .run(const ['symbolize', '--debug-info=app.debug', '--input=foo.stack', '--output=results/foo.result']); + .run(const ['symbolize', '--debug-info=$debugName', '--input=$inputName', '--output=$outputPath']); - expect(fileSystem.file('results/foo.result'), exists); - expect(fileSystem.file('results/foo.result').readAsBytesSync(), [104, 101, 108, 108, 111, 10]); // hello + expect(fileSystem.file(outputPath), exists); + expect(fileSystem.file(outputPath).readAsBytesSync(), [104, 101, 108, 108, 111, 10]); // hello + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize succeeds when DwarfSymbolizationService with a single --unit-id-debug-info argument for the root loading unit does not throw', () async { + const String debugName = 'app.debug'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); + + await createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$debugName', '--input=$inputName', '--output=$outputPath']); + + expect(fileSystem.file(outputPath), exists); + expect(fileSystem.file(outputPath).readAsBytesSync(), [104, 101, 108, 108, 111, 10]); // hello + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize succeeds when DwarfSymbolizationService with --debug-info and --unit-id-debug-info arguments does not throw', () async { + const String debugName = 'app.debug'; + const String debugName2 = '$debugName-2.part.so'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(debugName2).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); + + await createTestCommandRunner(command) + .run(const ['symbolize', '--debug-info=$debugName', '--unit-id-debug-info=${rootLoadingUnitId+1}:$debugName2', '--input=$inputName', '--output=$outputPath']); + + expect(fileSystem.file(outputPath), exists); + expect(fileSystem.file(outputPath).readAsBytesSync(), [104, 101, 108, 108, 111, 10]); // hello + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize succeeds when DwarfSymbolizationService with multiple --unit-id-debug-info arguments does not throw', () async { + const String debugName = 'app.debug'; + const String debugName2 = '$debugName-2.part.so'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(debugName2).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); + + await createTestCommandRunner(command) + .run(const ['symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$debugName', '--unit-id-debug-info=${rootLoadingUnitId+1}:$debugName2', '--input=$inputName', '--output=$outputPath']); + + expect(fileSystem.file(outputPath), exists); + expect(fileSystem.file(outputPath).readAsBytesSync(), [104, 101, 108, 108, 111, 10]); // hello }, overrides: { OutputPreferences: () => OutputPreferences.test(), }); testUsingContext('symbolize throws when DwarfSymbolizationService throws', () async { + const String debugName = 'app.debug'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, dwarfSymbolizationService: ThrowingDwarfSymbolizationService(), ); - fileSystem.file('app.debug').writeAsBytesSync([1, 2, 3]); - fileSystem.file('foo.stack').writeAsStringSync('hello'); + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); expect( createTestCommandRunner(command).run(const [ - 'symbolize', '--debug-info=app.debug', '--input=foo.stack', '--output=results/foo.result', + 'symbolize', '--debug-info=$debugName', '--input=$inputName', '--output=$outputPath', + ]), + throwsToolExit(message: 'test'), + ); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize throws when DwarfSymbolizationService with a single --unit-id-debug-info argument for the root loading unit throws', () async { + const String debugName = 'app.debug'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: ThrowingDwarfSymbolizationService(), + ); + + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); + + expect( + createTestCommandRunner(command).run(const [ + 'symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$debugName', '--input=$inputName', '--output=$outputPath', + ]), + throwsToolExit(message: 'test'), + ); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize throws when DwarfSymbolizationService with --debug-info and --unit-id-debug-info arguments throws', () async { + const String debugName = 'app.debug'; + const String debugName2 = '$debugName-2.part.so'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: ThrowingDwarfSymbolizationService(), + ); + + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(debugName2).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); + + expect( + createTestCommandRunner(command).run(const [ + 'symbolize', '--debug-info=$debugName', '--unit-id-debug-info=${rootLoadingUnitId+1}:$debugName2', '--input=$inputName', '--output=$outputPath', + ]), + throwsToolExit(message: 'test'), + ); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + + testUsingContext('symbolize throws when DwarfSymbolizationService with multiple --unit-id-debug-info arguments throws', () async { + const String debugName = 'app.debug'; + const String debugName2 = '$debugName-2.part.so'; + const String inputName = 'foo.stack'; + const String outputPath = 'results/foo.result'; + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: ThrowingDwarfSymbolizationService(), + ); + + fileSystem.file(debugName).writeAsBytesSync([1, 2, 3]); + fileSystem.file(debugName2).writeAsBytesSync([1, 2, 3]); + fileSystem.file(inputName).writeAsStringSync('hello'); + + expect( + createTestCommandRunner(command).run(const [ + 'symbolize', '--unit-id-debug-info=$rootLoadingUnitId:$debugName', '--unit-id-debug-info=${rootLoadingUnitId+1}:$debugName2', '--input=$inputName', '--output=$outputPath', ]), throwsToolExit(message: 'test'), ); @@ -150,10 +453,10 @@ void main() { class ThrowingDwarfSymbolizationService extends Fake implements DwarfSymbolizationService { @override - Future decode({ + Future decodeWithUnits({ required Stream> input, required IOSink output, - required Uint8List symbols, + required Map unitSymbols, }) async { throwToolExit('test'); }