202 lines
9.2 KiB
Dart
202 lines
9.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:integration_test/integration_test.dart';
|
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
|
import 'package:jj_mkvmaker/app/download_manager.dart';
|
|
import 'package:jj_mkvmaker/app/net_guard.dart';
|
|
import 'package:jj_mkvmaker/core/download_detect.dart' show DownloadKind;
|
|
import 'package:jj_mkvmaker/platform/windows/aria2_backend.dart';
|
|
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
|
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
|
import 'package:jj_mkvmaker/services/downloader.dart';
|
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
|
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'support/temp_dir.dart';
|
|
|
|
/// 사용자 질문 (10/20) "마그넷 테스트 된 건가" - 지금까지는 단위 시험뿐이었다. 실제 마그넷을 앱의 다운로드 관리자 (aria2c) 로 받는다.
|
|
/// 공개된 합법 배포 파일: Debian 공식 netinst ISO 의 토렌트 (cdimage.debian.org). 정보 해시는 공식 .torrent 에서 계산해
|
|
/// 마그넷을 만들고 (판이 바뀌어도 맞게), 끝나면 공식 SHA256SUMS 와 맞춘다.
|
|
/// - DNS 시스템: 끝까지 받기 · 진행률 · 속도 · 일시 정지 / 재개 · SHA256
|
|
/// - DNS 강제 (8.8.8.8 · 8.8.4.4, 앱 안 프록시): 받기 시작 · 진행률 · 일시 정지 / 재개 · 취소 (받던 파일 지움)
|
|
/// 인터넷 · 디스크 (약 800MB) 를 쓰므로 JJ_REAL_MAGNET=1 일 때만:
|
|
/// flutter test integration_test\magnet_real_test.dart -d windows (PowerShell: $env:JJ_REAL_MAGNET='1')
|
|
/// 받은 파일은 시험 폴더 (%TEMP%\jj_magnet_*) 에 두고 끝나면 지운다.
|
|
void main() {
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
const base = 'https://cdimage.debian.org/debian-cd/current/amd64/bt-cd/';
|
|
|
|
for (final forced in [false, true]) {
|
|
testWidgets('마그넷 실제 받기 (DNS ${forced ? '강제 8.8.8.8' : '시스템'})', (tester) async {
|
|
if (Platform.environment['JJ_REAL_MAGNET'] != '1') return markTestSkipped('JJ_REAL_MAGNET=1 일 때만 (인터넷 · 약 800MB)');
|
|
final aria2c = p.join(Directory.current.path, 'third_party', 'tools', 'windows', 'aria2c.exe');
|
|
expect(File(aria2c).existsSync(), isTrue, reason: '앱에 든 aria2c (tool\\fetch_tools.ps1)');
|
|
final dir = testTempDir('jj_magnet_');
|
|
void log(String s) => debugPrint('MAGNET ${forced ? 'DNS강제' : 'DNS시스템'} ${DateTime.now().toIso8601String().substring(11, 19)} $s');
|
|
|
|
// 공식 토렌트 · SHA256 (판 이름은 목록에서)
|
|
late Uint8List torrent;
|
|
late String isoName, want;
|
|
await tester.runAsync(() async {
|
|
final http = HttpClient();
|
|
Future<List<int>> get(String url) async {
|
|
final res = await (await http.getUrl(Uri.parse(url))).close();
|
|
expect(res.statusCode, 200, reason: url);
|
|
return [for (final c in await res.toList()) ...c];
|
|
}
|
|
|
|
final index = utf8.decode(await get(base));
|
|
final name = RegExp(r'href="(debian-[0-9.]+-amd64-netinst\.iso)\.torrent"').firstMatch(index)![1]!;
|
|
isoName = name;
|
|
torrent = Uint8List.fromList(await get('$base$name.torrent'));
|
|
final sums = utf8.decode(await get('${base}SHA256SUMS'));
|
|
want = RegExp('^([0-9a-f]{64})\\s+${RegExp.escape(name)}\$', multiLine: true).firstMatch(sums)![1]!;
|
|
http.close();
|
|
});
|
|
final (infoHash, announce) = _torrentInfo(torrent);
|
|
final magnet = 'magnet:?xt=urn:btih:$infoHash&dn=${Uri.encodeComponent(isoName)}&tr=${Uri.encodeComponent(announce)}';
|
|
log('$isoName · $infoHash · 공식 SHA256 $want');
|
|
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
c.settings
|
|
..download.root = dir.path
|
|
..network.mode = forced ? 'custom' : 'system'
|
|
..network.primary = '8.8.8.8'
|
|
..network.secondary = '8.8.4.4';
|
|
final g = NetGuard.instance
|
|
..config = (() => c.settings.network.config)
|
|
..log = log;
|
|
await tester.runAsync(g.ensure);
|
|
addTearDown(() async {
|
|
c.settings.network.mode = 'system';
|
|
await g.ensure();
|
|
});
|
|
final d = DownloadManager(backends: [Aria2Backend(aria2c: aria2c)], settings: () => c.settings);
|
|
await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d)));
|
|
|
|
Future<void> wait(int ms) async {
|
|
for (var i = 0; i < ms ~/ 250; i++) {
|
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 250)));
|
|
await tester.pump();
|
|
}
|
|
}
|
|
|
|
Future<bool> until(bool Function() ok, {required Duration within}) async {
|
|
final end = DateTime.now().add(within);
|
|
while (!ok() && DateTime.now().isBefore(end)) {
|
|
await wait(1000);
|
|
}
|
|
return ok();
|
|
}
|
|
|
|
// 받기 시작 (메타데이터 → 실제 받기) · 진행률 · 속도
|
|
final t = d.add(magnet);
|
|
expect(t, isNotNull);
|
|
expect(t!.kind, DownloadKind.torrent);
|
|
final speeds = <String>{};
|
|
final started = await until(() {
|
|
if (t.speed.isNotEmpty) speeds.add(t.speed);
|
|
return (t.receivedBytes ?? 0) >= 8 << 20 || t.state == DownloadState.failed;
|
|
}, within: const Duration(minutes: 5));
|
|
log('시작: ${t.state} ${t.receivedBytes}/${t.totalBytes} 속도 $speeds 오류 ${t.error}');
|
|
expect(started && t.state == DownloadState.downloading, isTrue, reason: '8MB 를 5분 안에 받지 못함: ${t.state} ${t.error}');
|
|
expect(t.title, isoName, reason: '메타데이터를 받으면 이름이 바뀜');
|
|
expect(t.progress, greaterThan(0));
|
|
expect(t.totalBytes, greaterThan(100 << 20));
|
|
expect(speeds, isNotEmpty, reason: '속도가 보임');
|
|
expect(find.text(isoName), findsWidgets, reason: '다운로드 화면 줄');
|
|
|
|
// 일시 정지: 받은 크기가 늘지 않는다
|
|
await tester.runAsync(() => d.pause([t]));
|
|
await wait(3000);
|
|
final paused = t.receivedBytes ?? 0;
|
|
final onDisk = t.files.where((f) => File(f).existsSync()).map((f) => File(f).lengthSync()).fold(0, (a, b) => a + b);
|
|
await wait(5000);
|
|
log('일시 정지: ${t.state} $paused');
|
|
expect(t.state, DownloadState.paused);
|
|
expect(t.receivedBytes ?? 0, paused);
|
|
expect(t.files.where((f) => File(f).existsSync()).map((f) => File(f).lengthSync()).fold(0, (a, b) => a + b), onDisk,
|
|
reason: '멈춘 동안 파일이 커지지 않음');
|
|
|
|
// 재개: 이어서 받는다 (처음부터가 아니라)
|
|
await tester.runAsync(() => d.resume([t]));
|
|
final resumed = await until(() => (t.receivedBytes ?? 0) >= paused + (4 << 20), within: const Duration(minutes: 3));
|
|
log('재개: ${t.state} ${t.receivedBytes}');
|
|
expect(resumed, isTrue, reason: '재개 뒤 4MB 더 받지 못함');
|
|
|
|
if (!forced) {
|
|
// 끝까지 · 공식 SHA256
|
|
final done = await until(() => t.state == DownloadState.done || t.state == DownloadState.failed,
|
|
within: const Duration(minutes: 30));
|
|
log('끝: ${t.state} ${t.files}');
|
|
expect(done && t.state == DownloadState.done, isTrue, reason: '${t.state} ${t.error}');
|
|
final iso = t.files.firstWhere((f) => f.endsWith(isoName));
|
|
final got = await tester.runAsync(() async => '${await sha256.bind(File(iso).openRead()).first}');
|
|
log('SHA256 $got');
|
|
expect(got, want, reason: '공식 SHA256SUMS 와 같음');
|
|
} else {
|
|
// 취소: 받던 파일 (와 .aria2) 이 지워진다
|
|
final files = [...t.files];
|
|
await tester.runAsync(() => d.cancel([t]));
|
|
await wait(2000);
|
|
log('취소: ${t.state} 남은 파일 ${files.where((f) => File(f).existsSync() || File('$f.aria2').existsSync()).toList()}');
|
|
expect(t.state, DownloadState.cancelled);
|
|
for (final f in files) {
|
|
expect(File(f).existsSync() || File('$f.aria2').existsSync(), isFalse, reason: '$f 지워짐');
|
|
}
|
|
}
|
|
await tester.runAsync(d.shutdown);
|
|
await tester.pumpWidget(const SizedBox());
|
|
await wait(500);
|
|
}, timeout: const Timeout(Duration(minutes: 45)));
|
|
}
|
|
}
|
|
|
|
/// .torrent (bencode) 에서 정보 해시 (info 사전 바이트의 SHA-1) 와 첫 tracker
|
|
(String, String) _torrentInfo(Uint8List b) {
|
|
var i = 0;
|
|
String str() {
|
|
final colon = b.indexOf(58, i); // ':'
|
|
final n = int.parse(ascii.decode(b.sublist(i, colon)));
|
|
final s = latin1.decode(b.sublist(colon + 1, colon + 1 + n));
|
|
i = colon + 1 + n;
|
|
return s;
|
|
}
|
|
|
|
void skip() {
|
|
final c = b[i];
|
|
if (c == 0x69) {
|
|
i = b.indexOf(0x65, i) + 1; // i…e
|
|
} else if (c == 0x6c || c == 0x64) {
|
|
i++;
|
|
while (b[i] != 0x65) {
|
|
if (c == 0x64) str();
|
|
skip();
|
|
}
|
|
i++;
|
|
} else {
|
|
str();
|
|
}
|
|
}
|
|
|
|
String? hash, announce;
|
|
i = 1; // 맨 위 사전 'd'
|
|
while (b[i] != 0x65) {
|
|
final key = str();
|
|
final start = i;
|
|
if (key == 'announce') {
|
|
announce = utf8.decode(latin1.encode(str()));
|
|
continue;
|
|
}
|
|
skip();
|
|
if (key == 'info') hash = '${sha1.convert(b.sublist(start, i))}';
|
|
}
|
|
return (hash!, announce ?? '');
|
|
}
|