blob: c72fdeafd1d67cbcac62e43ef6e698f2c35a4c92 [file] [log] [blame]
// Copyright (c) 2019, 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 'dart:io';
import 'dart:isolate';
import 'package:path/path.dart' as p;
/// Parse and yield data cases (each a [DataCase]) from [path].
Iterable<DataCase> dataCasesInFile(
{required String path, String? baseDir}) sync* {
final file = p.basename(path).replaceFirst(RegExp(r'\..+$'), '');
baseDir ??= p.relative(p.dirname(path), from: p.dirname(p.dirname(path)));
// Explicitly create a File, in case the entry is a Link.
final lines = File(path).readAsLinesSync();
final frontMatter = StringBuffer();
var i = 0;
while (!lines[i].startsWith('>>>')) {
frontMatter.write('${lines[i++]}\n');
}
while (i < lines.length) {
var description = lines[i++].replaceFirst(RegExp(r'>>>\s*'), '').trim();
final skip = description.startsWith('skip:');
if (description == '') {
description = 'line ${i + 1}';
} else {
description = 'line ${i + 1}: $description';
}
var input = '';
while (!lines[i].startsWith('<<<')) {
input += lines[i++] + '\n';
}
var expectedOutput = '';
while (++i < lines.length && !lines[i].startsWith('>>>')) {
expectedOutput += lines[i] + '\n';
}
final dataCase = DataCase(
directory: baseDir,
file: file,
front_matter: frontMatter.toString(),
description: description,
skip: skip,
input: input,
expectedOutput: expectedOutput);
yield dataCase;
}
}
/// Parse and return data cases (each a [DataCase]) from [directory].
///
/// By default, only read data cases from files with a `.unit` extension. Data
/// cases are read from files located immediately in [directory], or
/// recursively, according to [recursive].
Iterable<DataCase> _dataCases({
required String directory,
String extension = 'unit',
bool recursive = true,
}) {
final entries =
Directory(directory).listSync(recursive: recursive, followLinks: false);
final results = <DataCase>[];
for (final entry in entries) {
if (!entry.path.endsWith(extension)) {
continue;
}
final relativeDir =
p.relative(p.dirname(entry.path), from: p.dirname(directory));
results.addAll(dataCasesInFile(path: entry.path, baseDir: relativeDir));
}
// The API makes no guarantees on order. This is just here for stability in
// tests.
results.sort((a, b) {
final compare = a.directory.compareTo(b.directory);
if (compare != 0) return compare;
return a.file.compareTo(b.file);
});
return results;
}
/// Parse and yield data cases (each a [DataCase]) from the directory containing
/// [library], optionally under [subdirectory].
///
/// By default, only read data cases from files with a `.unit` extension. Data
/// cases are read from files located immediately in [directory], or
/// recursively, according to [recursive].
///
/// The typical use case of this method is to declare a library at the top of a
/// Dart test file, then reference the symbol with a pound sign. Example:
///
/// ```dart
/// library my_package.test.this_test;
///
/// import 'package:expected_output/expected_output.dart';
/// import 'package:test/test.dart';
///
/// void main() {
/// for (final dataCase
/// in dataCasesUnder(library: #my_package.test.this_test)) {
/// // ...
/// }
/// }
/// ```
Stream<DataCase> dataCasesUnder({
required String testDirectory,
String extension = 'unit',
bool recursive = true,
}) async* {
final markdownLibRoot = p.dirname((await Isolate.resolvePackageUri(
Uri.parse('package:markdown/markdown.dart')))!
.toFilePath());
final directory =
p.joinAll([p.dirname(markdownLibRoot), 'test', testDirectory]);
for (final dataCase in _dataCases(
directory: directory, extension: extension, recursive: recursive)) {
yield dataCase;
}
}
/// All of the data pertaining to a particular test case, namely the [input] and
/// [expectedOutput].
class DataCase {
final String directory;
final String file;
// ignore: non_constant_identifier_names
final String front_matter;
final String description;
final bool skip;
final String input;
final String expectedOutput;
DataCase({
this.directory = '',
this.file = '',
// ignore: non_constant_identifier_names
this.front_matter = '',
this.description = '',
this.skip = false,
required this.input,
required this.expectedOutput,
});
/// A good standard description for `test()`, derived from the data directory,
/// the particular data file, and the test case description.
String get testDescription => [directory, file, description].join(' ');
}