| // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:async'; |
| |
| import 'package:expect/expect.dart'; |
| import 'package:native_stack_traces/elf.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'use_flag_test_helper.dart'; |
| |
| // Used to ensure we don't have multiple equivalent calls to test. |
| final _seenDescriptions = <String>{}; |
| |
| Future<void> testAOT( |
| String dillPath, { |
| bool useAsm = false, |
| bool forceDrops = false, |
| bool stripUtil = false, // Note: forced true if useAsm. |
| bool stripFlag = false, |
| bool disassemble = false, |
| }) async { |
| if (const bool.fromEnvironment('dart.vm.product') && disassemble) { |
| Expect.isFalse(disassemble, 'no use of disassembler in PRODUCT mode'); |
| } |
| |
| final analyzeSnapshot = path.join(buildDir, 'analyze_snapshot'); |
| |
| // For assembly, we can't test the sizes of the snapshot sections, since we |
| // don't have a Mach-O reader for Mac snapshots and for ELF, the assembler |
| // merges the text/data sections and the VM/isolate section symbols may not |
| // have length information. Thus, we force external stripping so we can test |
| // the approximate size of the stripped snapshot. |
| if (useAsm) { |
| stripUtil = true; |
| } |
| |
| final descriptionBuilder = StringBuffer()..write(useAsm ? 'assembly' : 'elf'); |
| if (forceDrops) { |
| descriptionBuilder.write('-dropped'); |
| } |
| if (stripFlag) { |
| descriptionBuilder.write('-intstrip'); |
| } |
| if (stripUtil) { |
| descriptionBuilder.write('-extstrip'); |
| } |
| if (disassemble) { |
| descriptionBuilder.write('-disassembled'); |
| } |
| |
| final description = descriptionBuilder.toString(); |
| Expect.isTrue( |
| _seenDescriptions.add(description), |
| "test configuration $description would be run multiple times", |
| ); |
| |
| await withTempDir('analyze_snapshot_binary-$description', ( |
| String tempDir, |
| ) async { |
| // Generate the snapshot |
| final snapshotPath = path.join(tempDir, 'test.snap'); |
| final commonSnapshotArgs = [ |
| if (stripFlag) '--strip', // gen_snapshot specific and not a VM flag. |
| if (forceDrops) ...[ |
| '--dwarf-stack-traces', |
| '--no-retain-function-objects', |
| '--no-retain-code-objects', |
| ], |
| if (disassemble) '--disassemble', // Not defined in PRODUCT mode. |
| dillPath, |
| ]; |
| |
| if (useAsm) { |
| final assemblyPath = path.join(tempDir, 'test.S'); |
| |
| await run(genSnapshot, <String>[ |
| '--snapshot-kind=app-aot-assembly', |
| '--assembly=$assemblyPath', |
| ...commonSnapshotArgs, |
| ]); |
| |
| await assembleSnapshot(assemblyPath, snapshotPath); |
| } else { |
| await run(genSnapshot, <String>[ |
| '--snapshot-kind=app-aot-elf', |
| '--elf=$snapshotPath', |
| ...commonSnapshotArgs, |
| ]); |
| } |
| |
| print("Snapshot generated at $snapshotPath."); |
| |
| // May not be ELF, but another format. |
| final elf = Elf.fromFile(snapshotPath); |
| if (!useAsm) { |
| Expect.isNotNull(elf); |
| } |
| |
| if (elf != null) { |
| // Verify some ELF file format parameters. |
| final textSections = elf.namedSections(".text"); |
| Expect.isNotEmpty(textSections); |
| Expect.isTrue( |
| textSections.length <= 2, |
| "More text sections than expected", |
| ); |
| final dataSections = elf.namedSections(".rodata"); |
| Expect.isNotEmpty(dataSections); |
| Expect.isTrue( |
| dataSections.length <= 2, |
| "More data sections than expected", |
| ); |
| } |
| |
| final analyzerOutputPath = path.join(tempDir, 'analyze_test.json'); |
| |
| // This will throw if exit code is not 0. |
| await run(analyzeSnapshot, <String>[ |
| '--out=$analyzerOutputPath', |
| '$snapshotPath', |
| ]); |
| |
| final analyzerJsonBytes = await readFile(analyzerOutputPath); |
| final analyzerJson = json.decode(analyzerJsonBytes); |
| Expect.isFalse(analyzerJson.isEmpty); |
| Expect.isTrue( |
| analyzerJson.keys.toSet().containsAll([ |
| 'snapshot_data', |
| 'objects', |
| 'metadata', |
| ]), |
| ); |
| |
| final objects = (analyzerJson['objects'] as List) |
| .map((o) => o as Map) |
| .toList(); |
| final classes = objects.where((o) => o['type'] == 'Class').toList(); |
| final classnames = <int, String>{}; |
| final superclass = <int, int>{}; |
| final implementedInterfaces = <int, List<int>>{}; |
| for (final klass in classes) { |
| final id = klass['id']; |
| superclass[id] = klass['super_class'] as int; |
| classnames[id] = klass['name']; |
| implementedInterfaces[id] = [ |
| for (final superTypeId in klass['interfaces'] as List? ?? []) |
| objects[superTypeId]['type_class']!, |
| ]; |
| } |
| |
| // Find MethodChannel class. |
| final methodChannelId = classnames.entries |
| .singleWhere((e) => e.value == 'MethodChannel') |
| .key; |
| |
| // Find string instance. |
| final stringList = objects |
| .where((o) => o['type'] == 'String' && o['value'] == 'constChannel1') |
| .toList(); |
| Expect.isTrue( |
| stringList.length == 1, |
| 'one "constChannel1" string must exist in output', |
| ); |
| final int stringObjId = stringList.first['id']; |
| |
| // Find MethodChannel instance. |
| final instanceList = objects |
| .where( |
| (o) => |
| o['type'] == 'Instance' && |
| o['class'] == methodChannelId && |
| o['references'].contains(stringObjId), |
| ) |
| .toList(); |
| Expect.isTrue(instanceList.length == 1, '''one instance of MethodChannel |
| with reference to "constChannel1" must exist in output'''); |
| |
| // Test class hierarchy information |
| final myBaseClassId = classnames.entries |
| .singleWhere((e) => e.value == 'MyBase') |
| .key; |
| final mySubClassId = classnames.entries |
| .singleWhere((e) => e.value == 'MySub') |
| .key; |
| final myInterfaceClassId = classnames.entries |
| .singleWhere((e) => e.value == 'MyInterface') |
| .key; |
| |
| Expect.equals(myBaseClassId, superclass[mySubClassId]); |
| Expect.equals( |
| myInterfaceClassId, |
| implementedInterfaces[mySubClassId]!.single, |
| ); |
| |
| Expect.isTrue( |
| analyzerJson['metadata'].containsKey('analyzer_version'), |
| 'snapshot analyzer version must be reported', |
| ); |
| Expect.isTrue( |
| analyzerJson['metadata']['analyzer_version'] == 2, |
| 'invalid snapshot analyzer version', |
| ); |
| }); |
| } |
| |
| main() async { |
| void printSkip(String description) => print( |
| 'Skipping $description for ${path.basename(buildDir)} ' |
| 'on ${Platform.operatingSystem}' + |
| (clangBuildToolsDir == null ? ' without //buildtools' : ''), |
| ); |
| |
| // We don't have access to the SDK on Android. |
| if (Platform.isAndroid) { |
| printSkip('all tests'); |
| return; |
| } |
| |
| await withTempDir('analyze_snapshot_binary', (String tempDir) async { |
| // We only need to generate the dill file once for all JIT tests. |
| final _thisTestPath = path.join( |
| sdkDir, |
| 'runtime', |
| 'tests', |
| 'vm', |
| 'dart', |
| 'analyze_snapshot_program.dart', |
| ); |
| |
| // We only need to generate the dill file once for all AOT tests. |
| final aotDillPath = path.join(tempDir, 'aot_test.dill'); |
| await run(genKernel, <String>[ |
| '--aot', |
| '--platform', |
| platformDill, |
| ...Platform.executableArguments.where( |
| (arg) => |
| arg.startsWith('--enable-experiment=') || |
| arg == '--sound-null-safety' || |
| arg == '--no-sound-null-safety', |
| ), |
| '-o', |
| aotDillPath, |
| _thisTestPath, |
| ]); |
| |
| // Just as a reminder for AOT tests: |
| // * If useAsm is true, then stripUtil is forced (as the assembler may add |
| // extra information that needs stripping), so no need to specify |
| // stripUtil for useAsm tests. |
| |
| await Future.wait([ |
| // Test unstripped ELF generation directly. |
| testAOT(aotDillPath), |
| testAOT(aotDillPath, forceDrops: true), |
| |
| // Test flag-stripped ELF generation. |
| testAOT(aotDillPath, stripFlag: true), |
| ]); |
| |
| // Since we can't force disassembler support after the fact when running |
| // in PRODUCT mode, skip any --disassemble tests. Do these tests last as |
| // they have lots of output and so the log will be truncated. |
| if (!const bool.fromEnvironment('dart.vm.product')) { |
| // Regression test for dartbug.com/41149. |
| await Future.wait([testAOT(aotDillPath, disassemble: true)]); |
| } |
| |
| // Test unstripped ELF generation that is then externally stripped. |
| await Future.wait([testAOT(aotDillPath, stripUtil: true)]); |
| |
| // Dont test assembled snapshot for simulated platforms |
| if (!buildDir.endsWith("SIMARM64") && !buildDir.endsWith("SIMARM64C")) { |
| await Future.wait([ |
| // Test unstripped assembly generation that is then externally stripped. |
| testAOT(aotDillPath, useAsm: true), |
| // Test stripped assembly generation that is then externally stripped. |
| testAOT(aotDillPath, useAsm: true, stripFlag: true), |
| ]); |
| } |
| }); |
| } |
| |
| Future<String> readFile(String file) { |
| return new File(file).readAsString(); |
| } |