Skip to content

Commit

Permalink
Add an archive descriptor (dart-lang/test_descriptor#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 authored Jun 12, 2019
1 parent 139dfc9 commit bf11985
Show file tree
Hide file tree
Showing 8 changed files with 512 additions and 26 deletions.
4 changes: 2 additions & 2 deletions pkgs/test_descriptor/.travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: dart

dart:
- dev
- 2.0.0
- stable

dart_task:
- test
Expand All @@ -14,7 +14,7 @@ matrix:
- dart: dev
dart_task:
dartanalyzer: --fatal-infos --fatal-warnings .
- dart: 2.0.0
- dart: stable
dart_task:
dartanalyzer: --fatal-warnings .

Expand Down
5 changes: 5 additions & 0 deletions pkgs/test_descriptor/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.2.0

* Add an `ArchiveDescriptor` class and a corresponding `archive()` function that
can create and validate Zip and TAR archives.

## 1.1.1

* Update to lowercase Dart core library constants.
Expand Down
180 changes: 180 additions & 0 deletions pkgs/test_descriptor/lib/src/archive_descriptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// 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:async';
import 'dart:io';

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

import 'descriptor.dart';
import 'directory_descriptor.dart';
import 'file_descriptor.dart';
import 'sandbox.dart';
import 'utils.dart';

/// A [Descriptor] describing files in a Tar or Zip archive.
///
/// The format is determined by the descriptor's file extension.
@sealed
class ArchiveDescriptor extends Descriptor implements FileDescriptor {
/// Descriptors for entries in this archive.
final List<Descriptor> contents;

/// Returns a `package:archive` [Archive] object that contains the contents of
/// this file.
Future<Archive> get archive async {
var archive = Archive();
(await _files(contents)).forEach(archive.addFile);
return archive;
}

File get io => File(p.join(sandbox, name));

/// Returns [ArchiveFile]s for each file in [descriptors].
///
/// If [parent] is passed, it's used as the parent directory for filenames.
Future<Iterable<ArchiveFile>> _files(Iterable<Descriptor> descriptors,
[String parent]) async {
return (await waitAndReportErrors(descriptors.map((descriptor) async {
var fullName =
parent == null ? descriptor.name : "$parent/${descriptor.name}";

if (descriptor is FileDescriptor) {
var bytes = await collectBytes(descriptor.readAsBytes());
return [
ArchiveFile(fullName, bytes.length, bytes)
// Setting the mode and mod time are necessary to work around
// brendan-duncan/archive#76.
..mode = 428
..lastModTime = DateTime.now().millisecondsSinceEpoch ~/ 1000
];
} else if (descriptor is DirectoryDescriptor) {
return await _files(descriptor.contents, fullName);
} else {
throw UnsupportedError(
"An archive can only be created from FileDescriptors and "
"DirectoryDescriptors.");
}
})))
.expand((files) => files);
}

ArchiveDescriptor(String name, Iterable<Descriptor> contents)
: contents = List.unmodifiable(contents),
super(name);

Future create([String parent]) async {
var path = p.join(parent ?? sandbox, name);
var file = File(path).openWrite();
try {
try {
await readAsBytes().listen(file.add).asFuture();
} finally {
await file.close();
}
} catch (_) {
await File(path).delete();
rethrow;
}
}

Future<String> read() async => throw UnsupportedError(
"ArchiveDescriptor.read() is not supported. Use Archive.readAsBytes() "
"instead.");

Stream<List<int>> readAsBytes() => Stream.fromFuture(() async {
return _encodeFunction()(await archive);
}());

Future<void> validate([String parent]) async {
// Access this first so we eaerly throw an error for a path with an invalid
// extension.
var decoder = _decodeFunction();

var fullPath = p.join(parent ?? sandbox, name);
var pretty = prettyPath(fullPath);
if (!(await File(fullPath).exists())) {
fail('File not found: "$pretty".');
}

var bytes = await File(fullPath).readAsBytes();
Archive archive;
try {
archive = decoder(bytes);
} catch (_) {
// Catch every error to work around brendan-duncan/archive#77.
fail('File "$pretty" is not a valid archive.');
}

// Because validators expect to validate against a real filesystem, we have
// to extract the archive to a temp directory and run validation on that.
var tempDir = await Directory.systemTemp
.createTempSync('dart_test_')
.resolveSymbolicLinks();

try {
await waitAndReportErrors(archive.files.map((file) async {
var path = p.join(tempDir, file.name);
await Directory(p.dirname(path)).create(recursive: true);
await File(path).writeAsBytes(file.content as List<int>);
}));

await waitAndReportErrors(contents.map((entry) async {
try {
await entry.validate(tempDir);
} on TestFailure catch (error) {
// Replace the temporary directory with the path to the archive to
// make the error more user-friendly.
fail(error.message.replaceAll(tempDir, pretty));
}
}));
} finally {
await Directory(tempDir).delete(recursive: true);
}
}

/// Returns the function to use to encode this file to binary, based on its
/// [name].
List<int> Function(Archive) _encodeFunction() {
if (name.endsWith(".zip")) {
return ZipEncoder().encode;
} else if (name.endsWith(".tar")) {
return TarEncoder().encode;
} else if (name.endsWith(".tar.gz") ||
name.endsWith(".tar.gzip") ||
name.endsWith(".tgz")) {
return (archive) => GZipEncoder().encode(TarEncoder().encode(archive));
} else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) {
return (archive) => BZip2Encoder().encode(TarEncoder().encode(archive));
} else {
throw UnsupportedError("Unknown file format $name.");
}
}

/// Returns the function to use to decode this file from binary, based on its
/// [name].
Archive Function(List<int>) _decodeFunction() {
if (name.endsWith(".zip")) {
return ZipDecoder().decodeBytes;
} else if (name.endsWith(".tar")) {
return TarDecoder().decodeBytes;
} else if (name.endsWith(".tar.gz") ||
name.endsWith(".tar.gzip") ||
name.endsWith(".tgz")) {
return (archive) =>
TarDecoder().decodeBytes(GZipDecoder().decodeBytes(archive));
} else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) {
return (archive) =>
TarDecoder().decodeBytes(BZip2Decoder().decodeBytes(archive));
} else {
throw UnsupportedError("Unknown file format $name.");
}
}

String describe() => describeDirectory(name, contents);
}
21 changes: 1 addition & 20 deletions pkgs/test_descriptor/lib/src/directory_descriptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'dart:io';

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

import 'descriptor.dart';
Expand Down Expand Up @@ -122,23 +121,5 @@ class DirectoryDescriptor extends Descriptor {
}));
}

String describe() {
if (contents.isEmpty) return name;

var buffer = StringBuffer();
buffer.writeln(name);
for (var entry in contents.take(contents.length - 1)) {
var entryString =
prefixLines(entry.describe(), '${glyph.verticalLine} ',
first: '${glyph.teeRight}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.writeln(entryString);
}

var lastEntryString = prefixLines(contents.last.describe(), ' ',
first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.write(lastEntryString);
return buffer.toString();
}
String describe() => describeDirectory(name, contents);
}
24 changes: 23 additions & 1 deletion pkgs/test_descriptor/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'package:test/test.dart';

import 'descriptor.dart';
import 'sandbox.dart';

/// A UTF-8 codec that allows malformed byte sequences.
Expand All @@ -25,6 +26,27 @@ String addBullet(String text) =>
/// Converts [strings] to a bulleted list.
String bullet(Iterable<String> strings) => strings.map(addBullet).join("\n");

/// Returns a human-readable description of a directory with the given [name]
/// and [contents].
String describeDirectory(String name, List<Descriptor> contents) {
if (contents.isEmpty) return name;

var buffer = StringBuffer();
buffer.writeln(name);
for (var entry in contents.take(contents.length - 1)) {
var entryString = prefixLines(entry.describe(), '${glyph.verticalLine} ',
first: '${glyph.teeRight}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.writeln(entryString);
}

var lastEntryString = prefixLines(contents.last.describe(), ' ',
first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.write(lastEntryString);
return buffer.toString();
}

/// Prepends each line in [text] with [prefix].
///
/// If [first] or [last] is passed, the first and last lines, respectively, are
Expand Down Expand Up @@ -67,7 +89,7 @@ bool matchesAll(Pattern pattern, String string) =>

/// Like [Future.wait] with `eagerError: true`, but reports errors after the
/// first using [registerException] rather than silently ignoring them.
Future waitAndReportErrors(Iterable<Future> futures) {
Future<List<T>> waitAndReportErrors<T>(Iterable<Future<T>> futures) {
var errored = false;
return Future.wait(futures.map((future) {
// Avoid async/await so that we synchronously add error handlers for the
Expand Down
15 changes: 15 additions & 0 deletions pkgs/test_descriptor/lib/test_descriptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

import 'src/archive_descriptor.dart';
import 'src/descriptor.dart';
import 'src/directory_descriptor.dart';
import 'src/file_descriptor.dart';
import 'src/nothing_descriptor.dart';
import 'src/pattern_descriptor.dart';
import 'src/sandbox.dart';

export 'src/archive_descriptor.dart';
export 'src/descriptor.dart';
export 'src/directory_descriptor.dart';
export 'src/file_descriptor.dart';
Expand Down Expand Up @@ -72,5 +74,18 @@ PatternDescriptor filePattern(Pattern name, [contents]) =>
PatternDescriptor dirPattern(Pattern name, [Iterable<Descriptor> contents]) =>
pattern(name, (realName) => dir(realName, contents));

/// Creates a new [ArchiveDescriptor] with [name] and [contents].
///
/// [Descriptor.create] creates an archive with the given files and directories
/// within it, and [Descriptor.validate] validates that the archive contains the
/// given contents. It *doesn't* require that no other children exist. To ensure
/// that a particular child doesn't exist, use [nothing].
///
/// The type of the archive is determined by [name]'s file extension. It
/// supports `.zip`, `.tar`, `.tar.gz`/`.tar.gzip`/`.tgz`, and
/// `.tar.bz2`/`.tar.bzip2` files.
ArchiveDescriptor archive(String name, [Iterable<Descriptor> contents]) =>
ArchiveDescriptor(name, contents ?? []);

/// Returns [path] within the [sandbox] directory.
String path(String path) => p.join(sandbox, path);
8 changes: 5 additions & 3 deletions pkgs/test_descriptor/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: test_descriptor
version: 1.1.1
version: 1.2.0
description: An API for defining and verifying directory structures.
author: Dart Team <[email protected]>
homepage: https://github.com/dart-lang/test_descriptor
Expand All @@ -8,12 +8,14 @@ environment:
sdk: '>=2.0.0 <3.0.0'

dependencies:
async: '>=1.10.0 <3.0.0'
archive: '^2.0.0'
async: '>=1.13.0 <3.0.0'
collection: '^1.5.0'
matcher: '^0.12.0'
meta: '^1.1.7'
path: '^1.0.0'
stack_trace: '^1.0.0'
test: '>=0.12.19 <2.0.0'
test: '^1.6.0'
term_glyph: '^1.0.0'

dev_dependencies:
Expand Down
Loading

0 comments on commit bf11985

Please sign in to comment.