425 lines
12 KiB
Dart
425 lines
12 KiB
Dart
/// 数据格式模块测试
|
|
///
|
|
/// 测试 Tile 序列化、KiCad 导入、增量保存功能
|
|
///
|
|
/// @date 2026-03-07
|
|
|
|
import 'dart:typed_data';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:mobile_eda/data/data_format.dart';
|
|
|
|
void main() {
|
|
// ============================================================================
|
|
// Tile 格式测试
|
|
// ============================================================================
|
|
|
|
group('TileSerializer', () {
|
|
test('should serialize simple design', () {
|
|
final design = {
|
|
'id': 'design-001',
|
|
'name': 'Test Circuit',
|
|
'version': '1.0.0',
|
|
'components': [
|
|
{
|
|
'id': 'comp-001',
|
|
'name': 'R1',
|
|
'type': 'resistor',
|
|
'value': '10k',
|
|
'position': {'x': 1000000, 'y': 2000000},
|
|
},
|
|
{
|
|
'id': 'comp-002',
|
|
'name': 'C1',
|
|
'type': 'capacitor',
|
|
'value': '100nF',
|
|
'position': {'x': 3000000, 'y': 4000000},
|
|
},
|
|
],
|
|
'metadata': {
|
|
'createdAt': 1234567890,
|
|
'updatedAt': 1234567890,
|
|
},
|
|
};
|
|
|
|
final bytes = designToTile(design);
|
|
|
|
expect(bytes, isNotNull);
|
|
expect(bytes.length, greaterThan(0));
|
|
expect(bytes.length, lessThan(1000)); // 压缩后应该很小
|
|
});
|
|
|
|
test('should deserialize to original design', () {
|
|
final originalDesign = {
|
|
'id': 'design-002',
|
|
'name': 'Round Trip Test',
|
|
'components': [
|
|
{'id': 'c1', 'name': 'R1', 'type': 'resistor', 'value': '1k'},
|
|
{'id': 'c2', 'name': 'R2', 'type': 'resistor', 'value': '2k'},
|
|
],
|
|
};
|
|
|
|
final bytes = designToTile(originalDesign);
|
|
final restoredDesign = tileToDesign(bytes);
|
|
|
|
expect(restoredDesign['id'], equals(originalDesign['id']));
|
|
expect(restoredDesign['name'], equals(originalDesign['name']));
|
|
expect(restoredDesign['components'], isA<List>());
|
|
expect(restoredDesign['components'].length, equals(2));
|
|
});
|
|
|
|
test('should handle empty design', () {
|
|
final emptyDesign = {
|
|
'id': 'empty',
|
|
'name': 'Empty',
|
|
'components': [],
|
|
'nets': [],
|
|
};
|
|
|
|
final bytes = designToTile(emptyDesign);
|
|
final restored = tileToDesign(bytes);
|
|
|
|
expect(restored['id'], equals('empty'));
|
|
expect(restored['components'], equals([]));
|
|
});
|
|
|
|
test('should compress repeated strings', () {
|
|
// 创建包含大量重复字符串的设计
|
|
final design = {
|
|
'id': 'test',
|
|
'components': List.generate(100, (i) => {
|
|
'id': 'comp-$i',
|
|
'name': 'R$i',
|
|
'type': 'resistor', // 重复字符串
|
|
'value': '10k', // 重复字符串
|
|
}),
|
|
};
|
|
|
|
final bytes = designToTile(design);
|
|
final jsonSize = design.toString().length;
|
|
final tileSize = bytes.length;
|
|
|
|
// Tile 格式应该显著小于 JSON
|
|
expect(tileSize, lessThan(jsonSize));
|
|
print('Compression: JSON=$jsonSize bytes, Tile=$tileSize bytes, ratio=${tileSize/jsonSize*100}%');
|
|
});
|
|
|
|
test('should handle coordinate delta encoding', () {
|
|
// 创建坐标连续的设计
|
|
final design = {
|
|
'id': 'trace-test',
|
|
'trace': {
|
|
'points': [
|
|
{'x': 0, 'y': 0},
|
|
{'x': 100, 'y': 100},
|
|
{'x': 200, 'y': 150},
|
|
{'x': 350, 'y': 200},
|
|
{'x': 500, 'y': 300},
|
|
],
|
|
},
|
|
};
|
|
|
|
final bytes = designToTile(design);
|
|
final restored = tileToDesign(bytes);
|
|
|
|
expect(restored['trace']['points'], isA<List>());
|
|
expect(restored['trace']['points'].length, equals(5));
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// KiCad 导入器测试
|
|
// ============================================================================
|
|
|
|
group('KicadImporter', () {
|
|
test('should parse simple kicad schematic', () {
|
|
final content = '''
|
|
(kicad_sch (version 20211014)
|
|
(component
|
|
(reference "R1")
|
|
(value "10k")
|
|
(footprint "Resistor_SMD:R_0805")
|
|
(at 100 200 0)
|
|
)
|
|
(component
|
|
(reference "C1")
|
|
(value "100nF")
|
|
(footprint "Capacitor_SMD:C_0805")
|
|
(at 300 400 90)
|
|
)
|
|
)
|
|
''';
|
|
|
|
final importer = KicadImporter();
|
|
final schematic = importer.import(content);
|
|
|
|
expect(schematic.version, equals('20211014'));
|
|
expect(schematic.components.length, equals(2));
|
|
expect(schematic.components[0].reference, equals('R1'));
|
|
expect(schematic.components[0].value, equals('10k'));
|
|
expect(schematic.components[0].x, equals(100));
|
|
expect(schematic.components[0].y, equals(200));
|
|
});
|
|
|
|
test('should convert kicad to EDA design', () {
|
|
final content = '''
|
|
(kicad_sch (version 20211014)
|
|
(component
|
|
(reference "U1")
|
|
(value "ATmega328P")
|
|
(footprint "Package_DIP:DIP-28")
|
|
)
|
|
(net "VCC"
|
|
(node (pin "U1" "VCC"))
|
|
)
|
|
(net "GND"
|
|
(node (pin "U1" "GND"))
|
|
)
|
|
)
|
|
''';
|
|
|
|
final design = importKicadSchematic(content);
|
|
|
|
expect(design['id'], isNotNull);
|
|
expect(design['name'], equals('Imported from KiCad'));
|
|
expect(design['tables']['components'], isA<List>());
|
|
expect(design['tables']['nets'], isA<List>());
|
|
});
|
|
|
|
test('should handle component with pins', () {
|
|
final content = '''
|
|
(kicad_sch (version 20211014)
|
|
(component
|
|
(reference "U1")
|
|
(value "ATmega328P")
|
|
(pin (at 0 0 0) (property "Name" "VCC") (property "Number" "7"))
|
|
(pin (at 0 10 0) (property "Name" "GND") (property "Number" "8"))
|
|
)
|
|
)
|
|
''';
|
|
|
|
final importer = KicadImporter();
|
|
final schematic = importer.import(content);
|
|
|
|
expect(schematic.components.length, equals(1));
|
|
expect(schematic.components[0].pins.length, equals(2));
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 增量保存测试
|
|
// ============================================================================
|
|
|
|
group('IncrementalSave', () {
|
|
test('should record operations', () {
|
|
final manager = createIncrementalSaveManager();
|
|
|
|
manager.recordOperation(AddComponentCommand(
|
|
component: {'id': 'c1', 'name': 'R1'},
|
|
));
|
|
|
|
expect(manager.history.canUndo, isTrue);
|
|
expect(manager.history.undoStackSize, equals(1));
|
|
});
|
|
|
|
test('should support undo', () {
|
|
final manager = createIncrementalSaveManager();
|
|
|
|
manager.recordOperation(AddComponentCommand(
|
|
component: {'id': 'c1', 'name': 'R1'},
|
|
));
|
|
|
|
expect(manager.history.canUndo, isTrue);
|
|
manager.history.undo();
|
|
expect(manager.history.canUndo, isFalse);
|
|
expect(manager.history.canRedo, isTrue);
|
|
});
|
|
|
|
test('should support redo', () {
|
|
final manager = createIncrementalSaveManager();
|
|
|
|
manager.recordOperation(AddComponentCommand(
|
|
component: {'id': 'c1', 'name': 'R1'},
|
|
));
|
|
|
|
manager.history.undo();
|
|
expect(manager.history.canRedo, isTrue);
|
|
|
|
manager.history.redo();
|
|
expect(manager.history.canRedo, isFalse);
|
|
expect(manager.history.canUndo, isTrue);
|
|
});
|
|
|
|
test('should create snapshot', () {
|
|
final manager = createIncrementalSaveManager();
|
|
|
|
final design = {
|
|
'id': 'test',
|
|
'components': [{'id': 'c1', 'name': 'R1'}],
|
|
};
|
|
|
|
manager.setCurrentState(design);
|
|
manager.createSnapshot(design);
|
|
|
|
expect(manager.lastSnapshot, isNotNull);
|
|
expect(manager.isDirty, isFalse);
|
|
});
|
|
|
|
test('should save and restore', () {
|
|
final manager = createIncrementalSaveManager();
|
|
|
|
final design = {
|
|
'id': 'test',
|
|
'components': [{'id': 'c1', 'name': 'R1'}],
|
|
};
|
|
|
|
manager.setCurrentState(design);
|
|
manager.createSnapshot(design);
|
|
|
|
// 添加一些操作
|
|
manager.recordOperation(MoveComponentCommand(
|
|
componentId: 'c1',
|
|
oldX: 0,
|
|
oldY: 0,
|
|
newX: 100,
|
|
newY: 100,
|
|
));
|
|
|
|
// 保存
|
|
final saveData = manager.save();
|
|
expect(saveData.snapshot, isNotNull);
|
|
expect(saveData.deltaLog.length, equals(1));
|
|
|
|
// 恢复
|
|
manager.restore(saveData);
|
|
expect(manager.currentState, isNotNull);
|
|
});
|
|
|
|
test('should serialize save data', () {
|
|
final saveData = IncrementalSaveData(
|
|
snapshot: {'id': 'test'},
|
|
deltaLog: [
|
|
{'type': 'componentMove', 'componentId': 'c1'},
|
|
],
|
|
timestamp: DateTime.now(),
|
|
);
|
|
|
|
final bytes = saveData.toBytes();
|
|
final restored = IncrementalSaveData.fromBytes(bytes);
|
|
|
|
expect(restored.snapshot, isNotNull);
|
|
expect(restored.deltaLog.length, equals(1));
|
|
expect(restored.timestamp.millisecondsSinceEpoch,
|
|
equals(saveData.timestamp.millisecondsSinceEpoch));
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 断点恢复测试
|
|
// ============================================================================
|
|
|
|
group('CheckpointManager', () {
|
|
test('should create checkpoint', () {
|
|
final manager = createCheckpointManager();
|
|
|
|
final state = {'id': 'test', 'value': 123};
|
|
manager.createCheckpoint('test_checkpoint', state, 'Test reason');
|
|
|
|
expect(manager.checkpoints.length, equals(1));
|
|
});
|
|
|
|
test('should restore from checkpoint', () {
|
|
final manager = createCheckpointManager();
|
|
|
|
final state = {'id': 'test', 'value': 123};
|
|
manager.createCheckpoint('before_change', state, 'Before modification');
|
|
|
|
final restored = manager.restoreFromCheckpoint('before_change');
|
|
|
|
expect(restored, isNotNull);
|
|
expect(restored!['id'], equals('test'));
|
|
expect(restored['value'], equals(123));
|
|
});
|
|
|
|
test('should restore from latest checkpoint', () {
|
|
final manager = createCheckpointManager();
|
|
|
|
manager.createCheckpoint('v1', {'version': 1}, 'First version');
|
|
manager.createCheckpoint('v2', {'version': 2}, 'Second version');
|
|
manager.createCheckpoint('v3', {'version': 3}, 'Third version');
|
|
|
|
final restored = manager.restoreFromLatest();
|
|
|
|
expect(restored, isNotNull);
|
|
expect(restored!['version'], equals(3));
|
|
});
|
|
|
|
test('should limit checkpoint count', () {
|
|
final manager = createCheckpointManager(maxCheckpoints: 3);
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
manager.createCheckpoint('v$i', {'version': i}, 'Version $i');
|
|
}
|
|
|
|
expect(manager.checkpoints.length, equals(3));
|
|
expect(manager.checkpoints[0].name, equals('v2'));
|
|
expect(manager.checkpoints[2].name, equals('v4'));
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 性能测试
|
|
// ============================================================================
|
|
|
|
group('Performance', () {
|
|
test('should serialize 100 components quickly', () {
|
|
final design = {
|
|
'id': 'perf-test',
|
|
'components': List.generate(100, (i) => {
|
|
'id': 'comp-$i',
|
|
'name': 'R$i',
|
|
'type': 'resistor',
|
|
'value': '${1000 + i}Ω',
|
|
'position': {'x': i * 1000, 'y': i * 1000},
|
|
}),
|
|
};
|
|
|
|
final sw = Stopwatch()..start();
|
|
final bytes = designToTile(design);
|
|
sw.stop();
|
|
|
|
print('Serialize 100 components: ${sw.elapsedMilliseconds}ms');
|
|
expect(sw.elapsedMilliseconds, lessThan(100));
|
|
|
|
sw.reset();
|
|
sw.start();
|
|
final restored = tileToDesign(bytes);
|
|
sw.stop();
|
|
|
|
print('Deserialize 100 components: ${sw.elapsedMilliseconds}ms');
|
|
expect(sw.elapsedMilliseconds, lessThan(100));
|
|
});
|
|
|
|
test('should compress large design', () {
|
|
final design = {
|
|
'id': 'large-test',
|
|
'components': List.generate(500, (i) => {
|
|
'id': 'comp-$i',
|
|
'name': 'C$i',
|
|
'type': 'capacitor',
|
|
'value': '100nF',
|
|
'footprint': '0805',
|
|
}),
|
|
};
|
|
|
|
final jsonSize = design.toString().length;
|
|
final bytes = designToTile(design);
|
|
final tileSize = bytes.length;
|
|
|
|
final ratio = tileSize / jsonSize * 100;
|
|
print('Compression: JSON=$jsonSize bytes, Tile=$tileSize bytes, ratio=${ratio.toStringAsFixed(1)}%');
|
|
|
|
expect(ratio, lessThan(50)); // 应该压缩到 50% 以下
|
|
});
|
|
});
|
|
}
|