JJ_MKVMaker/test/move_settled_test.dart

245 lines
13 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/transfer_job.dart';
import 'package:jj_mkvmaker/core/sync_preview.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/core/sync_tools.dart' show CopyMethod;
import 'package:jj_mkvmaker/core/webdav.dart';
import 'package:path/path.dart' as p;
import 'support/dav_server.dart';
/// 214: 앱이 직접 하는 "안의 것" 이동 (WebDAV 가 낄 때 · 현재 방식) 은 대상에 맞춘 원본 파일만 지운다.
/// 예전에는 맞춘 뒤 원본 폴더를 다시 읽어 모든 파일을 지웠다 (건너뛴 것 · 도중에 생긴 것 · 링크 너머까지).
void main() {
late Directory tmp;
late String src, dst;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_move_settled_');
src = p.join(tmp.path, 'src');
dst = p.join(tmp.path, 'dst');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
Directory(dst).createSync();
});
tearDown(() {
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
});
TransferJob moveJob({String options = ''}) =>
TransferJob(sources: [src], dest: dst, move: true, contents: true, options: options);
test('214 a: -u 로 건너뛴 파일 (대상이 더 새것) 은 원본에 남는다 (rsync 처럼)', () async {
final old = File(p.join(src, 'keep.txt'))..writeAsStringSync('old source');
old.setLastModifiedSync(DateTime(2020));
File(p.join(dst, 'keep.txt')).writeAsStringSync('newer target');
final job = moveJob(options: '-u');
await job.run();
expect(job.error, isNull);
expect(File(p.join(dst, 'a.txt')).readAsStringSync(), 'a');
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse, reason: '맞춘 것은 지움');
expect(old.existsSync(), isTrue, reason: '건너뛴 것은 남김 (예전에는 지웠다)');
expect(File(p.join(dst, 'keep.txt')).readAsStringSync(), 'newer target');
// 216 · 212: 원본에 남은 것을 다시 세어 끝 알림에
expect(job.leftInSource, 1);
expect(job.doneSummary(), contains('이동 끝: 1개 · src → dst'));
expect(job.doneSummary(), contains('1개는 원본에 남음 (건너뜀)'));
});
test('214 c: 맞추는 동안 원본에 새로 생긴 파일은 지우지 않는다', () async {
// 천천히 보내는 큰 파일 (맞추는 동안 새 파일을 만든다)
File(p.join(src, 'big.bin')).writeAsBytesSync(List.filled(300 * 1024, 7));
final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, bandwidthKBps: 400);
final running = job.run();
await Future<void>.delayed(const Duration(milliseconds: 250));
File(p.join(src, 'arrived.txt')).writeAsStringSync('new');
await running;
expect(job.error, isNull);
expect(File(p.join(dst, 'big.bin')).lengthSync(), 300 * 1024);
expect(File(p.join(src, 'big.bin')).existsSync(), isFalse);
expect(File(p.join(src, 'arrived.txt')).existsSync(), isTrue, reason: '복사하지 않은 것은 남김');
});
test('214 d: 원본 안의 링크 (junction) 는 따라가지 않는다 - 링크가 가리키는 바깥 폴더의 파일은 그대로', () async {
final outside = Directory(p.join(tmp.path, 'outside'))..createSync();
final precious = File(p.join(outside.path, 'precious.txt'))..writeAsStringSync('keep me');
try {
Link(p.join(src, 'jn')).createSync(outside.path);
} on FileSystemException {
markTestSkipped('이 환경에서는 링크를 만들 수 없음');
return;
}
final job = moveJob();
await job.run();
expect(job.error, isNull);
expect(precious.existsSync(), isTrue);
expect(precious.readAsStringSync(), 'keep me');
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse);
});
test('215: rsync 가 원본을 못 지우면 쉬운 말 + 오류 줄 (끝쪽) 을 보인다 - 전송한 파일 이름만 보이지 않게', () {
final out = [for (var i = 0; i < 15; i++) 'movie$i.mkv'];
final err = [
'sender failed to remove movie3.mkv: Permission denied (13)',
'sender failed to remove movie4.mkv: Permission denied (13)',
'rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1338) [sender=3.4.1]',
];
final text = rsyncFailureText(23, err, [...out, ...err]);
expect(text, startsWith('원본 파일을 지우지 못했습니다'));
expect(text, contains('rsync 종료 코드 23'));
expect(text, contains('Permission denied'));
expect(text, contains('(code 23)'));
expect(text, isNot(contains('movie10.mkv')), reason: '전송한 파일 이름 줄이 아니라 오류 줄');
// 원본 지우기와 상관없는 실패는 그 말을 붙이지 않는다
expect(rsyncFailureText(12, ['rsync error: error in rsync protocol data stream (code 12)'], const []), isNot(contains('원본 파일을 지우지')));
});
test('217: robocopy 이동 - 대상에 같은 파일이 있어도 원본에서 지운다 (예전에는 남기고 "옮겼습니다")', () async {
if (!Platform.isWindows) return;
// robocopy 가 "같음" 으로 보는 파일 (탐색기 · Copy-Item 복사처럼 속성까지 같은 것)
final r = Process.runSync('powershell', ['-NoProfile', '-Command', "Copy-Item -LiteralPath '${p.join(src, 'a.txt')}' -Destination '${p.join(dst, 'a.txt')}'"]);
expect(r.exitCode, 0);
final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, method: CopyMethod.robocopy, options: '/E');
await job.run();
expect(job.error, isNull);
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse, reason: '같은 파일도 옮김');
expect(File(p.join(dst, 'a.txt')).readAsStringSync(), 'a');
expect(job.leftInSource, 0);
});
test('217 다시: robocopy 이동 - 만든 시각만 다른 같은 파일 (robocopy 의 "Modified") 도 원본에서 지운다 (실제 Windows 에서 남던 것)', () async {
if (!Platform.isWindows) return;
final a = p.join(src, 'a.txt'), b = p.join(dst, 'a.txt');
final r = Process.runSync('powershell', [
'-NoProfile',
'-Command',
"Copy-Item -LiteralPath '$a' -Destination '$b'; (Get-Item -LiteralPath '$b').CreationTime = (Get-Date).AddMinutes(5); "
"(Get-Item -LiteralPath '$b').LastWriteTime = (Get-Item -LiteralPath '$a').LastWriteTime",
]);
expect(r.exitCode, 0, reason: '${r.stderr}');
final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, method: CopyMethod.robocopy, options: '/E /COPY:DAT /DCOPY:T /R:2 /W:2');
await job.run();
expect(job.error, isNull);
expect(File(a).existsSync(), isFalse, reason: 'Modified 도 옮김 (/IM)');
expect(File(b).readAsStringSync(), 'a');
expect(job.leftInSource, 0);
});
test('235 · 229: robocopy 이동도 받는 쪽이 더 새것이면 건너뜀 (환경 설정 → /XO) · 남은 것은 카드 기록에도 (남은 수 · 옮긴 수)', () async {
// 옵션 칸에는 /XO 가 없다 - 환경 설정 (skipNewer) 만으로 robocopy 가 건너뛴다
final old = File(p.join(src, 'old.txt'))..writeAsStringSync('o');
old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
File(p.join(dst, 'old.txt')).writeAsStringSync('new');
final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, method: Platform.isWindows ? CopyMethod.robocopy : CopyMethod.builtin,
options: Platform.isWindows ? '/E /COPY:DAT /DCOPY:T /R:2 /W:2' : '', skipNewer: true);
await job.run();
expect(job.error, isNull);
expect(File(p.join(dst, 'old.txt')).readAsStringSync(), 'new', reason: '대상의 더 새 파일 그대로');
expect(old.existsSync(), isTrue, reason: '건너뛴 원본은 남김');
expect(job.leftInSource, 1);
expect([job.doneFiles, job.allFiles], [job.allFiles - 1, job.allFiles], reason: '옮긴 수는 남은 것을 뺀다 (robocopy 는 다 했다고 셈)');
final t = CopyTask(id: '1', sources: [src], dest: dst, move: true).copyWith(lastResult: 'done', lastFiles: job.doneFiles, lastLeft: job.leftInSource);
expect(CopyTask.fromJson(t.toJson()).lastLeft, 1);
});
test('229: 앱 직접 이동 - 받는 쪽이 더 새것이면 덮어쓰지 않고 원본도 남긴다 (환경 설정 처음 값 켜짐)', () async {
expect(AppSettings.fromJson({}).copySkipNewer, isTrue);
expect(AppSettings.fromJson((AppSettings()..copySkipNewer = false).toJson()).copySkipNewer, isFalse);
final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src');
old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst');
final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, skipNewer: true);
await job.run();
expect(job.error, isNull);
expect(File(p.join(dst, 'older.txt')).readAsStringSync(), 'NEW version in dst', reason: '새 내용 그대로');
expect(old.existsSync(), isTrue, reason: '건너뛴 원본은 이동이라도 남김');
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse, reason: '나머지는 옮김');
expect(job.leftInSource, 1);
expect(job.doneSummary(), contains('1개는 원본에 남음'));
});
test('229: 건너뛰기를 끄면 (사용자가 고름) 예전처럼 덮어쓴다', () async {
final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src');
old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst');
final job = TransferJob(sources: [src], dest: dst, move: false, contents: true, skipNewer: false);
await job.run();
expect(File(p.join(dst, 'older.txt')).readAsStringSync(), 'old in src');
});
test('229: 미리 보기 - 건너뛰면 "받는 쪽이 더 새것 - 건너뜀", 아니면 덮어쓸 줄에 "더 새 것" 표시', () async {
final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src');
old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst');
final skip = await previewSync(src, dst, toRight: true, update: true);
expect([for (final x in skip) if (x.rel == 'older.txt') x.action], [PreviewAction.skipNewer]);
final over = await previewSync(src, dst, toRight: true);
final o = over.firstWhere((x) => x.rel == 'older.txt');
expect([o.action, o.newer], [PreviewAction.update, true]);
});
group('214 b: WebDAV', () {
late Directory remote;
late TestDavServer server;
setUp(() async {
remote = Directory(p.join(tmp.path, 'remote'))..createSync();
server = TestDavServer(remote);
await server.start();
final s = DavServer(id: 'nas', name: 'NAS', url: server.url, user: 'user', password: 'pass');
DavRegistry.configure([s]);
});
tearDown(() async {
DavRegistry.configure([]);
await server.stop();
});
test('214 b: 크기만 같고 내용이 다른 대상 (대상이 더 새것) 이 있어도, 이동은 다시 보내고 지운다 (내용을 잃지 않게)', () async {
HttpOverrides.global = null;
File(p.join(src, 'same.txt'))
..writeAsStringSync('AAAA')
..setLastModifiedSync(DateTime(2020));
File(p.join(remote.path, 'same.txt')).writeAsStringSync('BBBB'); // 같은 크기 · 더 새것 · 다른 내용
final job = TransferJob(sources: [src], dest: 'dav://nas/', move: true, contents: true);
await job.run();
expect(job.error, isNull);
expect(File(p.join(remote.path, 'same.txt')).readAsStringSync(), 'AAAA', reason: '원본 내용이 대상에 (예전에는 같다고 보고 원본만 지웠다)');
expect(File(p.join(src, 'same.txt')).existsSync(), isFalse);
expect(File(p.join(remote.path, 'a.txt')).readAsStringSync(), 'a');
});
test('229 (WebDAV 로): 받는 쪽이 더 새것이면 건너뛰고 원본 남김', () async {
HttpOverrides.global = null;
File(p.join(src, 'older.txt'))
..writeAsStringSync('old in src')
..setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
File(p.join(remote.path, 'older.txt')).writeAsStringSync('NEW version in dst');
final job = TransferJob(sources: [src], dest: 'dav://nas/', move: true, contents: true, skipNewer: true);
await job.run();
expect(job.error, isNull);
expect(File(p.join(remote.path, 'older.txt')).readAsStringSync(), 'NEW version in dst');
expect(File(p.join(src, 'older.txt')).existsSync(), isTrue);
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse);
});
test('214 d (WebDAV 로): 로컬 원본의 링크는 따라가지 않는다', () async {
HttpOverrides.global = null;
final outside = Directory(p.join(tmp.path, 'outside2'))..createSync();
final precious = File(p.join(outside.path, 'precious.txt'))..writeAsStringSync('keep me');
try {
Link(p.join(src, 'jn')).createSync(outside.path);
} on FileSystemException {
markTestSkipped('이 환경에서는 링크를 만들 수 없음');
return;
}
final job = TransferJob(sources: [src], dest: 'dav://nas/', move: true, contents: true);
await job.run();
expect(job.error, isNull);
expect(precious.existsSync(), isTrue);
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse);
});
});
}