| // 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.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']); | |
| } |