Files
clide/test/search/replace_engine_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

141 lines
4.7 KiB
Dart

/// Tests for the search-and-replace engine (T-53, per D-79).
library;
import 'dart:io';
import 'package:clide/src/files/ignore.dart';
import 'package:clide/src/search/match.dart';
import 'package:clide/src/search/replace_engine.dart';
import 'package:test/test.dart';
void main() {
group('applyToText', () {
test('literal replacement counts and substitutes', () {
final r = applyToText('foo foo bar', const SearchQuery(pattern: 'foo'), 'X');
expect(r.text, 'X X bar');
expect(r.count, 2);
});
test('literal is case-sensitive by default, case-insensitive on request', () {
expect(applyToText('Foo foo', const SearchQuery(pattern: 'foo'), 'X').count, 1);
expect(applyToText('Foo foo', const SearchQuery(pattern: 'foo', ignoreCase: true), 'X').count, 2);
});
test(r'literal replacement does not expand $ references', () {
final r = applyToText('foo', const SearchQuery(pattern: 'foo'), r'$1-lit');
expect(r.text, r'$1-lit');
});
test('regex replacement expands capture groups', () {
final r = applyToText('alpha beta', const SearchQuery(pattern: r'(\w+) (\w+)', regex: true), r'$2 $1');
expect(r.text, 'beta alpha');
expect(r.count, 1);
});
test(r'regex $& is the whole match and $$ is a literal dollar', () {
final r = applyToText('x=1', const SearchQuery(pattern: r'\d', regex: true), r'$$$&');
expect(r.text, r'x=$1');
});
test('no match leaves text unchanged with count 0', () {
final r = applyToText('abc', const SearchQuery(pattern: 'zzz'), 'X');
expect(r.text, 'abc');
expect(r.count, 0);
});
test('empty pattern is a no-op', () {
final r = applyToText('abc', const SearchQuery(pattern: ''), 'X');
expect(r.count, 0);
});
});
group('computeReplacements', () {
late Directory root;
setUp(() async {
root = await Directory.systemTemp.createTemp('clide-replace-');
File('${root.path}/a.dart').writeAsStringSync('final foo = 1;\nfinal bar = foo;\n');
File('${root.path}/b.txt').writeAsStringSync('no hits\n');
});
tearDown(() async => root.delete(recursive: true));
test('reports changed files with per-line before/after edits', () async {
final r = await computeReplacements(
root: root,
ignore: IgnoreSet([]),
query: const SearchQuery(pattern: 'foo'),
replacement: 'baz',
);
expect(r, hasLength(1));
final fr = r.single;
expect(fr.path, 'a.dart');
expect(fr.count, 2);
expect(fr.edits, hasLength(2));
expect(fr.edits.first.before, 'final foo = 1;');
expect(fr.edits.first.after, 'final baz = 1;');
});
test('files with no match are omitted', () async {
final r = await computeReplacements(
root: root,
ignore: IgnoreSet([]),
query: const SearchQuery(pattern: 'foo'),
replacement: 'baz',
);
expect(r.any((f) => f.path == 'b.txt'), isFalse);
});
test('honours the ignore set', () async {
final r = await computeReplacements(
root: root,
ignore: IgnoreSet.parse(const ['*.dart\n']),
query: const SearchQuery(pattern: 'foo'),
replacement: 'baz',
);
expect(r, isEmpty);
});
test('binary files are skipped', () async {
File('${root.path}/blob.bin').writeAsBytesSync([0x66, 0x6f, 0x6f, 0x00, 0x66, 0x6f, 0x6f]);
final r = await computeReplacements(
root: root,
ignore: IgnoreSet([]),
query: const SearchQuery(pattern: 'foo'),
replacement: 'baz',
);
expect(r.any((f) => f.path == 'blob.bin'), isFalse);
});
});
group('rewriteFileContent', () {
late Directory root;
setUp(() async {
root = await Directory.systemTemp.createTemp('clide-rewrite-');
File('${root.path}/a.dart').writeAsStringSync('foo and foo\n');
});
tearDown(() async => root.delete(recursive: true));
test('returns the rewritten content', () {
final out = rewriteFileContent(root.absolute.path, 'a.dart', const SearchQuery(pattern: 'foo'), 'X');
expect(out, 'X and X\n');
});
test('returns null when nothing changes', () {
final out = rewriteFileContent(root.absolute.path, 'a.dart', const SearchQuery(pattern: 'zzz'), 'X');
expect(out, isNull);
});
test('ReplacementEdit + FileReplacement round-trip JSON', () {
const fr = FileReplacement(
path: 'a.dart',
count: 1,
edits: [ReplacementEdit(line: 2, before: 'a', after: 'b')],
);
final back = FileReplacement.fromJson(fr.toJson());
expect(back.path, 'a.dart');
expect(back.count, 1);
expect(back.edits.single.line, 2);
expect(back.edits.single.after, 'b');
});
});
}