Skip to content

Commit bf11985

Browse files
authored
Add an archive descriptor (dart-archive/test_descriptor#21)
1 parent 139dfc9 commit bf11985

File tree

8 files changed

+512
-26
lines changed

8 files changed

+512
-26
lines changed

pkgs/test_descriptor/.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ language: dart
22

33
dart:
44
- dev
5-
- 2.0.0
5+
- stable
66

77
dart_task:
88
- test
@@ -14,7 +14,7 @@ matrix:
1414
- dart: dev
1515
dart_task:
1616
dartanalyzer: --fatal-infos --fatal-warnings .
17-
- dart: 2.0.0
17+
- dart: stable
1818
dart_task:
1919
dartanalyzer: --fatal-warnings .
2020

pkgs/test_descriptor/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.2.0
2+
3+
* Add an `ArchiveDescriptor` class and a corresponding `archive()` function that
4+
can create and validate Zip and TAR archives.
5+
16
## 1.1.1
27

38
* Update to lowercase Dart core library constants.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:archive/archive.dart';
9+
import 'package:async/async.dart';
10+
import 'package:meta/meta.dart';
11+
import 'package:path/path.dart' as p;
12+
import 'package:test/test.dart';
13+
14+
import 'descriptor.dart';
15+
import 'directory_descriptor.dart';
16+
import 'file_descriptor.dart';
17+
import 'sandbox.dart';
18+
import 'utils.dart';
19+
20+
/// A [Descriptor] describing files in a Tar or Zip archive.
21+
///
22+
/// The format is determined by the descriptor's file extension.
23+
@sealed
24+
class ArchiveDescriptor extends Descriptor implements FileDescriptor {
25+
/// Descriptors for entries in this archive.
26+
final List<Descriptor> contents;
27+
28+
/// Returns a `package:archive` [Archive] object that contains the contents of
29+
/// this file.
30+
Future<Archive> get archive async {
31+
var archive = Archive();
32+
(await _files(contents)).forEach(archive.addFile);
33+
return archive;
34+
}
35+
36+
File get io => File(p.join(sandbox, name));
37+
38+
/// Returns [ArchiveFile]s for each file in [descriptors].
39+
///
40+
/// If [parent] is passed, it's used as the parent directory for filenames.
41+
Future<Iterable<ArchiveFile>> _files(Iterable<Descriptor> descriptors,
42+
[String parent]) async {
43+
return (await waitAndReportErrors(descriptors.map((descriptor) async {
44+
var fullName =
45+
parent == null ? descriptor.name : "$parent/${descriptor.name}";
46+
47+
if (descriptor is FileDescriptor) {
48+
var bytes = await collectBytes(descriptor.readAsBytes());
49+
return [
50+
ArchiveFile(fullName, bytes.length, bytes)
51+
// Setting the mode and mod time are necessary to work around
52+
// brendan-duncan/archive#76.
53+
..mode = 428
54+
..lastModTime = DateTime.now().millisecondsSinceEpoch ~/ 1000
55+
];
56+
} else if (descriptor is DirectoryDescriptor) {
57+
return await _files(descriptor.contents, fullName);
58+
} else {
59+
throw UnsupportedError(
60+
"An archive can only be created from FileDescriptors and "
61+
"DirectoryDescriptors.");
62+
}
63+
})))
64+
.expand((files) => files);
65+
}
66+
67+
ArchiveDescriptor(String name, Iterable<Descriptor> contents)
68+
: contents = List.unmodifiable(contents),
69+
super(name);
70+
71+
Future create([String parent]) async {
72+
var path = p.join(parent ?? sandbox, name);
73+
var file = File(path).openWrite();
74+
try {
75+
try {
76+
await readAsBytes().listen(file.add).asFuture();
77+
} finally {
78+
await file.close();
79+
}
80+
} catch (_) {
81+
await File(path).delete();
82+
rethrow;
83+
}
84+
}
85+
86+
Future<String> read() async => throw UnsupportedError(
87+
"ArchiveDescriptor.read() is not supported. Use Archive.readAsBytes() "
88+
"instead.");
89+
90+
Stream<List<int>> readAsBytes() => Stream.fromFuture(() async {
91+
return _encodeFunction()(await archive);
92+
}());
93+
94+
Future<void> validate([String parent]) async {
95+
// Access this first so we eaerly throw an error for a path with an invalid
96+
// extension.
97+
var decoder = _decodeFunction();
98+
99+
var fullPath = p.join(parent ?? sandbox, name);
100+
var pretty = prettyPath(fullPath);
101+
if (!(await File(fullPath).exists())) {
102+
fail('File not found: "$pretty".');
103+
}
104+
105+
var bytes = await File(fullPath).readAsBytes();
106+
Archive archive;
107+
try {
108+
archive = decoder(bytes);
109+
} catch (_) {
110+
// Catch every error to work around brendan-duncan/archive#77.
111+
fail('File "$pretty" is not a valid archive.');
112+
}
113+
114+
// Because validators expect to validate against a real filesystem, we have
115+
// to extract the archive to a temp directory and run validation on that.
116+
var tempDir = await Directory.systemTemp
117+
.createTempSync('dart_test_')
118+
.resolveSymbolicLinks();
119+
120+
try {
121+
await waitAndReportErrors(archive.files.map((file) async {
122+
var path = p.join(tempDir, file.name);
123+
await Directory(p.dirname(path)).create(recursive: true);
124+
await File(path).writeAsBytes(file.content as List<int>);
125+
}));
126+
127+
await waitAndReportErrors(contents.map((entry) async {
128+
try {
129+
await entry.validate(tempDir);
130+
} on TestFailure catch (error) {
131+
// Replace the temporary directory with the path to the archive to
132+
// make the error more user-friendly.
133+
fail(error.message.replaceAll(tempDir, pretty));
134+
}
135+
}));
136+
} finally {
137+
await Directory(tempDir).delete(recursive: true);
138+
}
139+
}
140+
141+
/// Returns the function to use to encode this file to binary, based on its
142+
/// [name].
143+
List<int> Function(Archive) _encodeFunction() {
144+
if (name.endsWith(".zip")) {
145+
return ZipEncoder().encode;
146+
} else if (name.endsWith(".tar")) {
147+
return TarEncoder().encode;
148+
} else if (name.endsWith(".tar.gz") ||
149+
name.endsWith(".tar.gzip") ||
150+
name.endsWith(".tgz")) {
151+
return (archive) => GZipEncoder().encode(TarEncoder().encode(archive));
152+
} else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) {
153+
return (archive) => BZip2Encoder().encode(TarEncoder().encode(archive));
154+
} else {
155+
throw UnsupportedError("Unknown file format $name.");
156+
}
157+
}
158+
159+
/// Returns the function to use to decode this file from binary, based on its
160+
/// [name].
161+
Archive Function(List<int>) _decodeFunction() {
162+
if (name.endsWith(".zip")) {
163+
return ZipDecoder().decodeBytes;
164+
} else if (name.endsWith(".tar")) {
165+
return TarDecoder().decodeBytes;
166+
} else if (name.endsWith(".tar.gz") ||
167+
name.endsWith(".tar.gzip") ||
168+
name.endsWith(".tgz")) {
169+
return (archive) =>
170+
TarDecoder().decodeBytes(GZipDecoder().decodeBytes(archive));
171+
} else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) {
172+
return (archive) =>
173+
TarDecoder().decodeBytes(BZip2Decoder().decodeBytes(archive));
174+
} else {
175+
throw UnsupportedError("Unknown file format $name.");
176+
}
177+
}
178+
179+
String describe() => describeDirectory(name, contents);
180+
}

pkgs/test_descriptor/lib/src/directory_descriptor.dart

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'dart:io';
77

88
import 'package:async/async.dart';
99
import 'package:path/path.dart' as p;
10-
import 'package:term_glyph/term_glyph.dart' as glyph;
1110
import 'package:test/test.dart';
1211

1312
import 'descriptor.dart';
@@ -122,23 +121,5 @@ class DirectoryDescriptor extends Descriptor {
122121
}));
123122
}
124123

125-
String describe() {
126-
if (contents.isEmpty) return name;
127-
128-
var buffer = StringBuffer();
129-
buffer.writeln(name);
130-
for (var entry in contents.take(contents.length - 1)) {
131-
var entryString =
132-
prefixLines(entry.describe(), '${glyph.verticalLine} ',
133-
first: '${glyph.teeRight}${glyph.horizontalLine}'
134-
'${glyph.horizontalLine} ');
135-
buffer.writeln(entryString);
136-
}
137-
138-
var lastEntryString = prefixLines(contents.last.describe(), ' ',
139-
first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}'
140-
'${glyph.horizontalLine} ');
141-
buffer.write(lastEntryString);
142-
return buffer.toString();
143-
}
124+
String describe() => describeDirectory(name, contents);
144125
}

pkgs/test_descriptor/lib/src/utils.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;
99
import 'package:term_glyph/term_glyph.dart' as glyph;
1010
import 'package:test/test.dart';
1111

12+
import 'descriptor.dart';
1213
import 'sandbox.dart';
1314

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

29+
/// Returns a human-readable description of a directory with the given [name]
30+
/// and [contents].
31+
String describeDirectory(String name, List<Descriptor> contents) {
32+
if (contents.isEmpty) return name;
33+
34+
var buffer = StringBuffer();
35+
buffer.writeln(name);
36+
for (var entry in contents.take(contents.length - 1)) {
37+
var entryString = prefixLines(entry.describe(), '${glyph.verticalLine} ',
38+
first: '${glyph.teeRight}${glyph.horizontalLine}'
39+
'${glyph.horizontalLine} ');
40+
buffer.writeln(entryString);
41+
}
42+
43+
var lastEntryString = prefixLines(contents.last.describe(), ' ',
44+
first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}'
45+
'${glyph.horizontalLine} ');
46+
buffer.write(lastEntryString);
47+
return buffer.toString();
48+
}
49+
2850
/// Prepends each line in [text] with [prefix].
2951
///
3052
/// If [first] or [last] is passed, the first and last lines, respectively, are
@@ -67,7 +89,7 @@ bool matchesAll(Pattern pattern, String string) =>
6789

6890
/// Like [Future.wait] with `eagerError: true`, but reports errors after the
6991
/// first using [registerException] rather than silently ignoring them.
70-
Future waitAndReportErrors(Iterable<Future> futures) {
92+
Future<List<T>> waitAndReportErrors<T>(Iterable<Future<T>> futures) {
7193
var errored = false;
7294
return Future.wait(futures.map((future) {
7395
// Avoid async/await so that we synchronously add error handlers for the

pkgs/test_descriptor/lib/test_descriptor.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import 'package:path/path.dart' as p;
66
import 'package:test/test.dart';
77

8+
import 'src/archive_descriptor.dart';
89
import 'src/descriptor.dart';
910
import 'src/directory_descriptor.dart';
1011
import 'src/file_descriptor.dart';
1112
import 'src/nothing_descriptor.dart';
1213
import 'src/pattern_descriptor.dart';
1314
import 'src/sandbox.dart';
1415

16+
export 'src/archive_descriptor.dart';
1517
export 'src/descriptor.dart';
1618
export 'src/directory_descriptor.dart';
1719
export 'src/file_descriptor.dart';
@@ -72,5 +74,18 @@ PatternDescriptor filePattern(Pattern name, [contents]) =>
7274
PatternDescriptor dirPattern(Pattern name, [Iterable<Descriptor> contents]) =>
7375
pattern(name, (realName) => dir(realName, contents));
7476

77+
/// Creates a new [ArchiveDescriptor] with [name] and [contents].
78+
///
79+
/// [Descriptor.create] creates an archive with the given files and directories
80+
/// within it, and [Descriptor.validate] validates that the archive contains the
81+
/// given contents. It *doesn't* require that no other children exist. To ensure
82+
/// that a particular child doesn't exist, use [nothing].
83+
///
84+
/// The type of the archive is determined by [name]'s file extension. It
85+
/// supports `.zip`, `.tar`, `.tar.gz`/`.tar.gzip`/`.tgz`, and
86+
/// `.tar.bz2`/`.tar.bzip2` files.
87+
ArchiveDescriptor archive(String name, [Iterable<Descriptor> contents]) =>
88+
ArchiveDescriptor(name, contents ?? []);
89+
7590
/// Returns [path] within the [sandbox] directory.
7691
String path(String path) => p.join(sandbox, path);

pkgs/test_descriptor/pubspec.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test_descriptor
2-
version: 1.1.1
2+
version: 1.2.0
33
description: An API for defining and verifying directory structures.
44
author: Dart Team <[email protected]>
55
homepage: https://github.com/dart-lang/test_descriptor
@@ -8,12 +8,14 @@ environment:
88
sdk: '>=2.0.0 <3.0.0'
99

1010
dependencies:
11-
async: '>=1.10.0 <3.0.0'
11+
archive: '^2.0.0'
12+
async: '>=1.13.0 <3.0.0'
1213
collection: '^1.5.0'
1314
matcher: '^0.12.0'
15+
meta: '^1.1.7'
1416
path: '^1.0.0'
1517
stack_trace: '^1.0.0'
16-
test: '>=0.12.19 <2.0.0'
18+
test: '^1.6.0'
1719
term_glyph: '^1.0.0'
1820

1921
dev_dependencies:

0 commit comments

Comments
 (0)