Skip to content
This repository was archived by the owner on Apr 8, 2021. It is now read-only.

Initial code commit with basic functionality, some tests, and a README #1

Merged
merged 3 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.packages
.pub
pubspec.lock
37 changes: 37 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Want to contribute? Great! First, read this page (including the small print at
the end).

### Before you contribute

Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.

Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.

### Code reviews

All submissions, including submissions by project members, require review.

### File headers

All files in the project must start with the following header.

// Copyright (c) 2017, 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.

### The small print

Contributions made by corporations are covered by a different agreement than the
one above, the
[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate).
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
Keep multiline test case inputs and outputs out of your Dart test code, and
write them directly as text files.

## Example

Take the following test case for String's `toUpperCase()`:

```dart
void main() {
test('Works on multiline strings', () {
var input = r'''This is the first line.
This is another line.

This is another paragraph.''';

var expectedOutput = r'''THIS IS THE FIRST LINE.
THIS IS ANOTHER LINE.

THIS IS ANOTHER PARAGRAPH.''';

expect(input.toUpperCase(), equals(expectedOutput));
});
}
```

Multiline strings break the visual flow of code indentations. Additionally, when
newline characters are important, you may not be able to just write a newline
after `r'''`, or before the closing `''';`, which makes it hard to scan for
where multiline Strings begin and end. Instead, let's add write this test case,
and a few more, in a separate text file:

```none
>>> Works on simple strings
This is a single line.
<<<
THIS IS A SINGLE LINE.
>>> Does nothing to upper case strings
THIS IS ALREADY UPPER CASE.
<<<
THIS IS ALREADY UPPER CASE.
>>> Works on multiline strings
This is the first line.
This is another line.

This is another paragraph.
<<<
THIS IS THE FIRST LINE.
THIS IS ANOTHER LINE.

THIS IS ANOTHER PARAGRAPH.
```

We can quickly create tests over these data cases with some Dart:

```dart
library touppercase.tests;

void main() {
for (var dataCase in dataCasesUnder(library: #touppercase.tests)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit in using reflection vs. specifying a data directory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no other way to get a directory relative to the executing code. (Unless there recently is.) The mirrors incantation comes from dart-lang/test#110.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just put a little comment in the README about this.

test(dataCase.testDescription, () {
var actualOutput = dataCase.input.toUpperCase();
expect(actualOutput, equals(dataCase.expectedOutput));
});
}
}
```

If our test is located at `to_upper_case_package/test/to_upper_case_test.dart`,
then `dataCasesUnder` will look for files ending in `.unit` in the same
directory as the `library`. So our text file with the test cases should be,
perhaps, `to_upper_case_package/test/cases.unit`.

(Note: Why the weird library symbols? This is the simplest way to locate a
directory or file relative to Dart source. Hopefully a temporary issue.)

## When to use

This package is not very broad purposed, and is probably appropriate for only
very specific functionality testing. It has been very useful for testing
packages that primarily transform one block of text into another block of text.

This package is also mostly useful when the different ways to configure the
processing is very limited. In these cases, the Dart code that receives the test
cases and asserts over them can be very brief, and developers can focus on just
writing the data test cases.

This package is also most useful when whitespace is _ever_ significant. In cases
like this, multiline strings cannot fake indentation, and it may be harder for
the developer to track whitespace when writing a test case. In contrast, it is
probably much easier to write input and expected output in simple text blocks
in simple text files. Examples would include a Markdown parser that needs to
parse indented list continuations or indented code blocks, and text formatters
that need to test specific indentation in the output.

Disclaimer: this is not an official Google product.
108 changes: 108 additions & 0 deletions lib/expected_output.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:io';
import 'dart:mirrors';

import 'package:path/path.dart' as p;
import 'package:test/test.dart';

// TODO(srawlins): add a recursive option
/// Parse and yield data cases (each a [DataCase]) from [directory].
///
/// By default, only read data cases from files with a `.unit` extension.
Iterable<DataCase> dataCases({
String directory,
String extension: 'unit',
}) sync* {
var entries =
new Directory(directory).listSync(recursive: false, followLinks: false);
for (var entry in entries) {
if (!entry.path.endsWith(extension)) {
continue;
}

var file =
p.basename(entry.path).replaceFirst(new RegExp('\.$extension\$'), '');

// Explicitly create a File, in case the entry is a Link.
var lines = new File(entry.path).readAsLinesSync();

var i = 0;
while (i < lines.length) {
var description =
lines[i++].replaceFirst(new RegExp(r'>>>\s*'), '').trim();
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';
}

var dataCase = new DataCase(
directory: p.basename(directory),
file: file,
description: description,
input: input,
expectedOutput: expectedOutput);
yield dataCase;
}
}
}

/// Parse and yield data cases (each a [DataCase]) from the directory containing
/// [library].
///
/// By default, only read data cases from files with a `.unit` extension.
///
/// 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 (var dataCase in dataCasesUnder(library: #my_package.test.this_test)) {
/// // ...
/// }
/// }
/// ```
Iterable<DataCase> dataCasesUnder(
{Symbol library, String extension: 'unit'}) sync* {
var directory =
p.dirname(currentMirrorSystem().findLibrary(library).uri.toFilePath());
for (var dataCase
in expectedOutputs(directory: directory, extension: extension)) {
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;
final String description;
final String input;
final String expectedOutput;

DataCase(
{this.directory,
this.file,
this.description,
this.input,
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(' ');
}
22 changes: 22 additions & 0 deletions test/simple_data/cases.unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
>>>
input 1
<<<
output 1
>>> description with a space in the front.
input 2
<<<
output 2
>>>description without a space in the front.
input 3
<<<
output 3
>>> description with a few spaces in the front.
input 4
<<<
output 4
>>> multiline input and output
input
five
<<<
output
five
4 changes: 4 additions & 0 deletions test/simple_data/cases2.unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
>>> a second unit file
an input
<<<
an output
69 changes: 69 additions & 0 deletions test/simple_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
library expected_output.test.simple_test;

import 'dart:mirrors';

import 'package:expected_output/expected_output.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

void main() {
var iterator;
var dataCase;
var iteratorIsEmpty;

setUpAll(() {
// Locate the "test" directory. Use mirrors so that this works with the test
// package, which loads this suite into an isolate.
var testDir = p.dirname(currentMirrorSystem()
.findLibrary(#expected_output.test.simple_test)
.uri
.toFilePath());
iterator = dataCases(directory: p.join(testDir, 'simple_data')).iterator;
});

setUp(() {
iteratorIsEmpty = !iterator.moveNext();
dataCase = iterator.current;
});

test('parses case w/o a description', () {
expect(dataCase.directory, endsWith('simple_data'));
expect(dataCase.file, 'cases');
expect(dataCase.description, 'line 2');
expect(dataCase.testDescription, 'simple_data cases line 2');
expect(dataCase.input, 'input 1\n');
expect(dataCase.expectedOutput, 'output 1\n');
});

test('parses case w/ whitespace after >>>', () {
expect(
dataCase.description, 'line 6: description with a space in the front.');
});

test('parses case w/o whitespace after >>>', () {
expect(dataCase.description,
'line 10: description without a space in the front.');
});

test('parses case w/ multiple whitespace after >>>', () {
expect(dataCase.description,
'line 14: description with a few spaces in the front.');
});

test('parses case w/ multiline input and output', () {
expect(dataCase.input, 'input\nfive\n');
expect(dataCase.expectedOutput, 'output\nfive\n');
});

test('parses case w/o a description', () {
expect(dataCase.directory, endsWith('simple_data'));
expect(dataCase.file, 'cases2');
expect(dataCase.description, 'line 2: a second unit file');
expect(dataCase.testDescription,
'simple_data cases2 line 2: a second unit file');
});

test('the dataCases iterator is empty at the end', () {
expect(iteratorIsEmpty, isTrue);
});
}