blob: e91b26363bbdb4fe6411dca45bd747208f429d52 [file] [edit]
// Copyright (c) 2026, 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 'package:expect/expect.dart';
import 'package:wasm_builder/source_map.dart';
import 'package:wasm_builder/wasm_builder.dart';
void main() {
testEmpty();
testSingleMapping();
testSpecialOpcodeSameLine();
testSpecialOpcodeNextLine();
testGeneralLineColDeltas();
testFileAndNameChanges();
testUnmappedRegions();
testDeduplicationAndOverwrite();
testModuleSourceMapJson();
testSourceMapDeserializationRoundTrip();
}
void testEmpty() {
final debugInfo = DebugInfoTables();
final writer = DebugInfoWriter(debugInfo);
Expect.isTrue(writer.isEmpty);
final bytes = writer.build();
Expect.equals(0, bytes.length);
final reader = DebugInfoReader(bytes, debugInfo);
Expect.isFalse(reader.moveNext());
}
void testSingleMapping() {
final debugInfo = DebugInfoTables();
final fileUri = Uri.parse('file:///app/main.dart');
final writer = DebugInfoWriter(debugInfo);
writer.setSourcePosition(0, fileUri, 10, 5, 'myFunction');
final bytes = writer.build();
Expect.isFalse(writer.isEmpty);
final reader = DebugInfoReader(bytes, debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.isTrue(reader.hasSourcePosition);
Expect.equals(fileUri, reader.fileUri);
Expect.equals(0, reader.fileIndex);
Expect.equals(10, reader.line);
Expect.equals(5, reader.col);
Expect.equals('myFunction', reader.name);
Expect.equals(0, reader.nameIndex);
Expect.isFalse(reader.moveNext());
}
void testSpecialOpcodeSameLine() {
final debugInfo = DebugInfoTables();
final fileUri = Uri.parse('file:///app/main.dart');
final writer = DebugInfoWriter(debugInfo);
// First mapping (SET_ALL)
writer.setSourcePosition(0, fileUri, 10, 20, 'fn');
final firstLen = writer.build().length;
// Same line (deltaLine == 0), deltaOffset = 2, deltaCol = 5 -> should take 1 byte
final writer2 = DebugInfoWriter(debugInfo);
writer2.setSourcePosition(0, fileUri, 10, 20, 'fn');
writer2.setSourcePosition(2, fileUri, 10, 25, 'fn');
final secondLen = writer2.build().length;
Expect.equals(firstLen + 1, secondLen);
// Same line (deltaLine == 0), deltaOffset = 3, deltaCol = -7 -> should take 1 byte
final writer3 = DebugInfoWriter(debugInfo);
writer3.setSourcePosition(0, fileUri, 10, 20, 'fn');
writer3.setSourcePosition(2, fileUri, 10, 25, 'fn');
writer3.setSourcePosition(5, fileUri, 10, 18, 'fn');
final thirdLen = writer3.build().length;
Expect.equals(secondLen + 1, thirdLen);
final reader = DebugInfoReader(writer3.build(), debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.equals(10, reader.line);
Expect.equals(20, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(2, reader.offset);
Expect.equals(10, reader.line);
Expect.equals(25, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(5, reader.offset);
Expect.equals(10, reader.line);
Expect.equals(18, reader.col);
Expect.isFalse(reader.moveNext());
}
void testSpecialOpcodeNextLine() {
final debugInfo = DebugInfoTables();
final fileUri = Uri.parse('file:///app/main.dart');
final writer = DebugInfoWriter(debugInfo);
writer.setSourcePosition(0, fileUri, 10, 20, 'fn');
final firstLen = writer.build().length;
// Next line (deltaLine == 1), deltaOffset = 4, deltaCol = -2 -> should take 1 byte
final writer2 = DebugInfoWriter(debugInfo);
writer2.setSourcePosition(0, fileUri, 10, 20, 'fn');
writer2.setSourcePosition(4, fileUri, 11, 18, 'fn');
final secondLen = writer2.build().length;
Expect.equals(firstLen + 1, secondLen);
// Next line (deltaLine == 1), deltaOffset = 1, deltaCol = 3 -> should take 1 byte
final writer3 = DebugInfoWriter(debugInfo);
writer3.setSourcePosition(0, fileUri, 10, 20, 'fn');
writer3.setSourcePosition(4, fileUri, 11, 18, 'fn');
writer3.setSourcePosition(5, fileUri, 12, 21, 'fn');
final thirdLen = writer3.build().length;
Expect.equals(secondLen + 1, thirdLen);
final reader = DebugInfoReader(writer3.build(), debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.equals(10, reader.line);
Expect.equals(20, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(4, reader.offset);
Expect.equals(11, reader.line);
Expect.equals(18, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(5, reader.offset);
Expect.equals(12, reader.line);
Expect.equals(21, reader.col);
Expect.isFalse(reader.moveNext());
}
void testGeneralLineColDeltas() {
final debugInfo = DebugInfoTables();
final fileUri = Uri.parse('file:///app/main.dart');
final writer = DebugInfoWriter(debugInfo);
writer.setSourcePosition(0, fileUri, 100, 50, 'fn');
// Jump forward 50 lines, column +100, offset +200
writer.setSourcePosition(200, fileUri, 150, 150, 'fn');
// Jump backward 80 lines, column -90, offset +10
writer.setSourcePosition(210, fileUri, 70, 60, 'fn');
final reader = DebugInfoReader(writer.build(), debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.equals(100, reader.line);
Expect.equals(50, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(200, reader.offset);
Expect.equals(150, reader.line);
Expect.equals(150, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(210, reader.offset);
Expect.equals(70, reader.line);
Expect.equals(60, reader.col);
Expect.isFalse(reader.moveNext());
}
void testFileAndNameChanges() {
final debugInfo = DebugInfoTables();
final file1 = Uri.parse('file:///app/a.dart');
final file2 = Uri.parse('file:///app/b.dart');
final writer = DebugInfoWriter(debugInfo);
writer.setSourcePosition(0, file1, 10, 5, 'funcA');
writer.setSourcePosition(5, file2, 20, 8, 'funcB');
writer.setSourcePosition(10, file2, 22, 12, null);
final reader = DebugInfoReader(writer.build(), debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.equals(file1, reader.fileUri);
Expect.equals(0, reader.fileIndex);
Expect.equals('funcA', reader.name);
Expect.equals(0, reader.nameIndex);
Expect.equals(10, reader.line);
Expect.equals(5, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(5, reader.offset);
Expect.equals(file2, reader.fileUri);
Expect.equals(1, reader.fileIndex);
Expect.equals('funcB', reader.name);
Expect.equals(1, reader.nameIndex);
Expect.equals(20, reader.line);
Expect.equals(8, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(10, reader.offset);
Expect.equals(file2, reader.fileUri);
Expect.equals(1, reader.fileIndex);
Expect.isNull(reader.name);
Expect.equals(-1, reader.nameIndex);
Expect.equals(22, reader.line);
Expect.equals(12, reader.col);
Expect.isFalse(reader.moveNext());
}
void testUnmappedRegions() {
final debugInfo = DebugInfoTables();
final file = Uri.parse('file:///app/main.dart');
final writer = DebugInfoWriter(debugInfo);
writer.setSourcePosition(0, file, 10, 5, 'fn');
writer.clearSourcePosition(5);
writer.setSourcePosition(10, file, 15, 2, 'fn');
final reader = DebugInfoReader(writer.build(), debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.isTrue(reader.hasSourcePosition);
Expect.equals(10, reader.line);
Expect.isTrue(reader.moveNext());
Expect.equals(5, reader.offset);
Expect.isFalse(reader.hasSourcePosition);
Expect.isNull(reader.fileUri);
Expect.isTrue(reader.moveNext());
Expect.equals(10, reader.offset);
Expect.isTrue(reader.hasSourcePosition);
Expect.equals(15, reader.line);
Expect.equals(2, reader.col);
Expect.isFalse(reader.moveNext());
}
void testDeduplicationAndOverwrite() {
final debugInfo = DebugInfoTables();
final file = Uri.parse('file:///app/main.dart');
final writer = DebugInfoWriter(debugInfo);
writer.setSourcePosition(0, file, 10, 5, 'fn');
// Duplicate mapping at offset 5 with same location -> ignored
writer.setSourcePosition(5, file, 10, 5, 'fn');
// Overwrite mapping at offset 5 with new location
writer.setSourcePosition(5, file, 12, 8, 'fn');
// Duplicate unmapped
writer.clearSourcePosition(10);
writer.clearSourcePosition(12); // ignored because already unmapped
final reader = DebugInfoReader(writer.build(), debugInfo);
Expect.isTrue(reader.moveNext());
Expect.equals(0, reader.offset);
Expect.equals(10, reader.line);
Expect.equals(5, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(5, reader.offset);
Expect.equals(12, reader.line);
Expect.equals(8, reader.col);
Expect.isTrue(reader.moveNext());
Expect.equals(10, reader.offset);
Expect.isFalse(reader.hasSourcePosition);
Expect.isFalse(reader.moveNext());
}
void testModuleSourceMapJson() {
final file1 = Uri.parse('file:///app/a.dart');
final file2 = Uri.parse('file:///app/b.dart');
final moduleBuilder = ModuleBuilder('test', Uri.parse('test.wasm.map'));
final fnType = moduleBuilder.types.defineFunction(const [], const []);
final fn1 = moduleBuilder.functions.define(fnType);
final b1 = fn1.body;
b1.setSourcePosition(file1, 10, 0, 'funcA');
b1.i32_const(1);
b1.setSourcePosition(file1, 11, 4, 'funcA');
b1.drop();
b1.end();
fn1.build();
final fn2 = moduleBuilder.functions.define(fnType);
final b2 = fn2.body;
b2.setSourcePosition(file2, 20, 2, 'funcB');
b2.i32_const(2);
b2.clearSourcePosition();
b2.drop();
b2.end();
fn2.build();
final module = moduleBuilder.build();
final serializer = Serializer();
final sourceMapBuilder = SourceMapBuilder(module.debugInfoTables);
module.serialize(serializer, sourceMapBuilder);
final json = sourceMapBuilder.toJson();
Expect.equals(3, json['version']);
Expect.listEquals([
'file:///app/a.dart',
'file:///app/b.dart',
], json['sources'] as List);
Expect.listEquals(['funcA', 'funcB'], json['names'] as List);
Expect.type<String>(json['mappings']);
Expect.isTrue((json['mappings'] as String).isNotEmpty);
}
void testSourceMapDeserializationRoundTrip() {
final file1 = Uri.parse('file:///app/a.dart');
final file2 = Uri.parse('file:///app/b.dart');
final moduleBuilder = ModuleBuilder('test', Uri.parse('test.wasm.map'));
final fnType = moduleBuilder.types.defineFunction(const [], const []);
final fn1 = moduleBuilder.functions.define(fnType);
final b1 = fn1.body;
b1.setSourcePosition(file1, 10, 0, 'funcA');
b1.i32_const(1);
b1.setSourcePosition(file1, 11, 4, 'funcA');
b1.drop();
b1.end();
fn1.build();
final fn2 = moduleBuilder.functions.define(fnType);
final b2 = fn2.body;
b2.setSourcePosition(file2, 20, 2, 'funcB');
b2.i32_const(2);
b2.clearSourcePosition();
b2.drop();
b2.end();
fn2.build();
final originalModule = moduleBuilder.build();
final originalSerializer = Serializer();
final originalSourceMapBuilder = SourceMapBuilder(
originalModule.debugInfoTables,
);
originalModule.serialize(originalSerializer, originalSourceMapBuilder);
final originalBytes = originalSerializer.data;
final originalJson = originalSourceMapBuilder.toJson();
// Deserialize module with streaming SourceMapDecoder
final deserializedModule = Module.deserialize(
Deserializer(originalBytes),
debugInfoDeserializer: SourceMapDecoder.fromJson(originalJson),
);
// Verify deserialized module debug info
Expect.isNotNull(deserializedModule.debugInfoTables);
Expect.listEquals(
['file:///app/a.dart', 'file:///app/b.dart'],
deserializedModule.debugInfoTables!.files.map((u) => u.toString()).toList(),
);
Expect.listEquals([
'funcA',
'funcB',
], deserializedModule.debugInfoTables!.names);
// Verify function source mappings
final f1 = deserializedModule.functions.defined[0];
final r1 = DebugInfoReader(
f1.body.debugInfo!,
deserializedModule.debugInfoTables,
);
Expect.isTrue(r1.moveNext());
Expect.equals(0, r1.offset);
Expect.equals(file1, r1.fileUri);
Expect.equals(10, r1.line);
Expect.equals(0, r1.col);
Expect.equals('funcA', r1.name);
Expect.isTrue(r1.moveNext());
Expect.equals(1, r1.offset);
Expect.equals(file1, r1.fileUri);
Expect.equals(11, r1.line);
Expect.equals(4, r1.col);
Expect.equals('funcA', r1.name);
Expect.isFalse(r1.moveNext());
final f2 = deserializedModule.functions.defined[1];
final r2 = DebugInfoReader(
f2.body.debugInfo!,
deserializedModule.debugInfoTables,
);
Expect.isTrue(r2.moveNext());
Expect.equals(0, r2.offset);
Expect.equals(file2, r2.fileUri);
Expect.equals(20, r2.line);
Expect.equals(2, r2.col);
Expect.equals('funcB', r2.name);
Expect.isTrue(r2.moveNext());
Expect.equals(1, r2.offset);
Expect.isFalse(r2.hasSourcePosition);
Expect.isFalse(r2.moveNext());
// Serialize the deserialized module and compare generated Wasm bytes & source map JSON
final reserializer = Serializer();
final reserializedSourceMapBuilder = SourceMapBuilder(
deserializedModule.debugInfoTables,
);
deserializedModule.serialize(reserializer, reserializedSourceMapBuilder);
Expect.listEquals(originalBytes, reserializer.data);
final reserializedJson = reserializedSourceMapBuilder.toJson();
Expect.equals(originalJson['version'], reserializedJson['version']);
Expect.listEquals(
originalJson['sources'] as List,
reserializedJson['sources'] as List,
);
Expect.listEquals(
originalJson['names'] as List,
reserializedJson['names'] as List,
);
Expect.equals(originalJson['mappings'], reserializedJson['mappings']);
}