Skip to content

Commit 77e208c

Browse files
ntkmenex3
andauthoredSep 9, 2023
Run cli compilations in parallel dart isolates (#2078)
Co-authored-by: Natalie Weizenbaum <nweiz@google.com>
1 parent fddf421 commit 77e208c

File tree

13 files changed

+273
-160
lines changed

13 files changed

+273
-160
lines changed
 

‎bin/sass.dart

+8-69
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,21 @@ import 'package:path/path.dart' as p;
88
import 'package:stack_trace/stack_trace.dart';
99
import 'package:term_glyph/term_glyph.dart' as term_glyph;
1010

11-
import 'package:sass/src/exception.dart';
12-
import 'package:sass/src/executable/compile_stylesheet.dart';
11+
import 'package:sass/src/executable/concurrent.dart';
1312
import 'package:sass/src/executable/options.dart';
1413
import 'package:sass/src/executable/repl.dart';
1514
import 'package:sass/src/executable/watch.dart';
1615
import 'package:sass/src/import_cache.dart';
1716
import 'package:sass/src/io.dart';
18-
import 'package:sass/src/io.dart' as io;
1917
import 'package:sass/src/logger/deprecation_handling.dart';
2018
import 'package:sass/src/stylesheet_graph.dart';
21-
import 'package:sass/src/util/map.dart';
2219
import 'package:sass/src/utils.dart';
2320
import 'package:sass/src/embedded/executable.dart'
2421
// Never load the embedded protocol when compiling to JS.
2522
if (dart.library.js) 'package:sass/src/embedded/unavailable.dart'
2623
as embedded;
2724

2825
Future<void> main(List<String> args) async {
29-
var printedError = false;
30-
31-
// Prints [error] to stderr, along with a preceding newline if anything else
32-
// has been printed to stderr.
33-
//
34-
// If [trace] is passed, its terse representation is printed after the error.
35-
void printError(String error, StackTrace? stackTrace) {
36-
var buffer = StringBuffer();
37-
if (printedError) buffer.writeln();
38-
printedError = true;
39-
buffer.write(error);
40-
41-
if (stackTrace != null) {
42-
buffer.writeln();
43-
buffer.writeln();
44-
buffer.write(Trace.from(stackTrace).terse.toString().trimRight());
45-
}
46-
47-
io.printError(buffer);
48-
}
49-
5026
if (args case ['--embedded', ...var rest]) {
5127
embedded.main(rest);
5228
return;
@@ -84,37 +60,8 @@ Future<void> main(List<String> args) async {
8460
return;
8561
}
8662

87-
for (var (source, destination) in options.sourcesToDestinations.pairs) {
88-
try {
89-
await compileStylesheet(options, graph, source, destination,
90-
ifModified: options.update);
91-
} on SassException catch (error, stackTrace) {
92-
if (destination != null && !options.emitErrorCss) {
93-
_tryDelete(destination);
94-
}
95-
96-
printError(error.toString(color: options.color),
97-
options.trace ? getTrace(error) ?? stackTrace : null);
98-
99-
// Exit code 65 indicates invalid data per
100-
// https://www.freebsd.org/cgi/man.cgi?query=sysexits.
101-
//
102-
// We let exitCode 66 take precedence for deterministic behavior.
103-
if (exitCode != 66) exitCode = 65;
104-
if (options.stopOnError) return;
105-
} on FileSystemException catch (error, stackTrace) {
106-
var path = error.path;
107-
printError(
108-
path == null
109-
? error.message
110-
: "Error reading ${p.relative(path)}: ${error.message}.",
111-
options.trace ? getTrace(error) ?? stackTrace : null);
112-
113-
// Error 66 indicates no input.
114-
exitCode = 66;
115-
if (options.stopOnError) return;
116-
}
117-
}
63+
await compileStylesheets(options, graph, options.sourcesToDestinations,
64+
ifModified: options.update);
11865
} on UsageException catch (error) {
11966
print("${error.message}\n");
12067
print("Usage: sass <input.scss> [output.css]\n"
@@ -128,8 +75,11 @@ Future<void> main(List<String> args) async {
12875
if (options?.color ?? false) buffer.write('\u001b[0m');
12976
buffer.writeln();
13077
buffer.writeln(error);
131-
132-
printError(buffer.toString(), getTrace(error) ?? stackTrace);
78+
buffer.writeln();
79+
buffer.writeln();
80+
buffer.write(
81+
Trace.from(getTrace(error) ?? stackTrace).terse.toString().trimRight());
82+
printError(buffer);
13383
exitCode = 255;
13484
}
13585
}
@@ -154,14 +104,3 @@ Future<String> _loadVersion() async {
154104
.split(" ")
155105
.last;
156106
}
157-
158-
/// Delete [path] if it exists and do nothing otherwise.
159-
///
160-
/// This is a separate function to work around dart-lang/sdk#53082.
161-
void _tryDelete(String path) {
162-
try {
163-
deleteFile(path);
164-
} on FileSystemException {
165-
// If the file doesn't exist, that's fine.
166-
}
167-
}

‎lib/src/executable/compile_stylesheet.dart

+61-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:convert';
66

77
import 'package:path/path.dart' as p;
88
import 'package:source_maps/source_maps.dart';
9+
import 'package:stack_trace/stack_trace.dart';
910

1011
import '../async_import_cache.dart';
1112
import '../compile.dart';
@@ -30,8 +31,42 @@ import 'options.dart';
3031
/// If [ifModified] is `true`, only recompiles if [source]'s modification time
3132
/// or that of a file it imports is more recent than [destination]'s
3233
/// modification time. Note that these modification times are cached by [graph].
33-
Future<void> compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
34-
String? source, String? destination,
34+
///
35+
/// Returns `(exitCode, error, stackTrace)` when an error occurs.
36+
Future<(int, String, String?)?> compileStylesheet(ExecutableOptions options,
37+
StylesheetGraph graph, String? source, String? destination,
38+
{bool ifModified = false}) async {
39+
try {
40+
await _compileStylesheetWithoutErrorHandling(
41+
options, graph, source, destination,
42+
ifModified: ifModified);
43+
} on SassException catch (error, stackTrace) {
44+
if (destination != null && !options.emitErrorCss) {
45+
_tryDelete(destination);
46+
}
47+
var message = error.toString(color: options.color);
48+
49+
// Exit code 65 indicates invalid data per
50+
// https://www.freebsd.org/cgi/man.cgi?query=sysexits.
51+
return _getErrorWithStackTrace(
52+
65, message, options.trace ? getTrace(error) ?? stackTrace : null);
53+
} on FileSystemException catch (error, stackTrace) {
54+
var path = error.path;
55+
var message = path == null
56+
? error.message
57+
: "Error reading ${p.relative(path)}: ${error.message}.";
58+
59+
// Exit code 66 indicates no input.
60+
return _getErrorWithStackTrace(
61+
66, message, options.trace ? getTrace(error) ?? stackTrace : null);
62+
}
63+
return null;
64+
}
65+
66+
/// Like [compileStylesheet], but throws errors instead of handling them
67+
/// internally.
68+
Future<void> _compileStylesheetWithoutErrorHandling(ExecutableOptions options,
69+
StylesheetGraph graph, String? source, String? destination,
3570
{bool ifModified = false}) async {
3671
var importer = FilesystemImporter('.');
3772
if (ifModified) {
@@ -150,7 +185,7 @@ Future<void> compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
150185
buffer.write('Compiled $sourceName to $destinationName.');
151186
if (options.color) buffer.write('\u001b[0m');
152187

153-
print(buffer);
188+
safePrint(buffer);
154189
}
155190

156191
/// Writes the source map given by [mapping] to disk (if necessary) according to
@@ -195,3 +230,26 @@ String _writeSourceMap(
195230
return (options.style == OutputStyle.compressed ? '' : '\n\n') +
196231
'/*# sourceMappingURL=$escapedUrl */';
197232
}
233+
234+
/// Delete [path] if it exists and do nothing otherwise.
235+
///
236+
/// This is a separate function to work around dart-lang/sdk#53082.
237+
void _tryDelete(String path) {
238+
try {
239+
deleteFile(path);
240+
} on FileSystemException {
241+
// If the file doesn't exist, that's fine.
242+
}
243+
}
244+
245+
/// Return a Record of `(exitCode, error, stackTrace)` for the given error.
246+
(int, String, String?) _getErrorWithStackTrace(
247+
int exitCode, String error, StackTrace? stackTrace) {
248+
return (
249+
exitCode,
250+
error,
251+
stackTrace != null
252+
? Trace.from(stackTrace).terse.toString().trimRight()
253+
: null
254+
);
255+
}

‎lib/src/executable/concurrent.dart

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'dart:math' as math;
6+
7+
import '../io.dart';
8+
import '../stylesheet_graph.dart';
9+
import '../util/map.dart';
10+
import 'compile_stylesheet.dart';
11+
import 'concurrent/vm.dart'
12+
// Never load the isolate library when compiling to JS.
13+
if (dart.library.js) 'concurrent/js.dart';
14+
import 'options.dart';
15+
16+
/// Compiles the stylesheets concurrently and returns whether all stylesheets are compiled
17+
/// successfully.
18+
Future<bool> compileStylesheets(ExecutableOptions options,
19+
StylesheetGraph graph, Map<String?, String?> sourcesToDestinations,
20+
{bool ifModified = false}) async {
21+
var errorsWithStackTraces = switch ([...sourcesToDestinations.pairs]) {
22+
// Concurrency does add some overhead, so avoid it in the common case of
23+
// compiling a single stylesheet.
24+
[(var source, var destination)] => [
25+
await compileStylesheet(options, graph, source, destination,
26+
ifModified: ifModified)
27+
],
28+
var pairs => await Future.wait([
29+
for (var (source, destination) in pairs)
30+
compileStylesheetConcurrently(options, graph, source, destination,
31+
ifModified: ifModified)
32+
], eagerError: options.stopOnError)
33+
};
34+
35+
var printedError = false;
36+
37+
// Print all errors in deterministic order.
38+
for (var errorWithStackTrace in errorsWithStackTraces) {
39+
if (errorWithStackTrace == null) continue;
40+
var (code, error, stackTrace) = errorWithStackTrace;
41+
42+
// We let the highest exitCode take precedence for deterministic behavior.
43+
exitCode = math.max(exitCode, code);
44+
45+
_printError(error, stackTrace, printedError);
46+
printedError = true;
47+
}
48+
49+
return !printedError;
50+
}
51+
52+
// Prints [error] to stderr, along with a preceding newline if anything else
53+
// has been printed to stderr.
54+
//
55+
// If [stackTrace] is passed, it is printed after the error.
56+
void _printError(String error, String? stackTrace, bool printedError) {
57+
var buffer = StringBuffer();
58+
if (printedError) buffer.writeln();
59+
buffer.write(error);
60+
if (stackTrace != null) {
61+
buffer.writeln();
62+
buffer.writeln();
63+
buffer.write(stackTrace);
64+
}
65+
printError(buffer);
66+
}

‎lib/src/executable/concurrent/js.dart

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import '../compile_stylesheet.dart';
6+
7+
/// We don't currently support concurrent compilation in JS.
8+
///
9+
/// In the future, we could add support using web workers.
10+
final compileStylesheetConcurrently = compileStylesheet;

‎lib/src/executable/concurrent/vm.dart

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'dart:isolate';
6+
7+
import 'package:term_glyph/term_glyph.dart' as term_glyph;
8+
9+
import '../options.dart';
10+
import '../../stylesheet_graph.dart';
11+
import '../compile_stylesheet.dart';
12+
13+
/// Compiles the stylesheet at [source] to [destination].
14+
///
15+
/// Runs in a new Dart Isolate, unless [source] is `null`.
16+
Future<(int, String, String?)?> compileStylesheetConcurrently(
17+
ExecutableOptions options,
18+
StylesheetGraph graph,
19+
String? source,
20+
String? destination,
21+
{bool ifModified = false}) {
22+
// Reading from stdin does not work properly in dart isolate.
23+
if (source == null) {
24+
return compileStylesheet(options, graph, source, destination,
25+
ifModified: ifModified);
26+
}
27+
28+
return Isolate.run(() {
29+
term_glyph.ascii = !options.unicode;
30+
return compileStylesheet(options, graph, source, destination,
31+
ifModified: ifModified);
32+
});
33+
}

‎lib/src/executable/watch.dart

+41-78
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5-
import 'dart:collection';
6-
75
import 'package:path/path.dart' as p;
8-
import 'package:stack_trace/stack_trace.dart';
96
import 'package:stream_transform/stream_transform.dart';
107
import 'package:watcher/watcher.dart';
118

12-
import '../exception.dart';
139
import '../importer/filesystem.dart';
1410
import '../io.dart';
1511
import '../stylesheet_graph.dart';
1612
import '../util/map.dart';
1713
import '../util/multi_dir_watcher.dart';
18-
import '../utils.dart';
19-
import 'compile_stylesheet.dart';
14+
import 'concurrent.dart';
2015
import 'options.dart';
2116

2217
/// Watches all the files in [graph] for changes and updates them as necessary.
@@ -41,15 +36,17 @@ Future<void> watch(ExecutableOptions options, StylesheetGraph graph) async {
4136
// they currently exist. This ensures that changes that come in update a
4237
// known-good state.
4338
var watcher = _Watcher(options, graph);
44-
for (var (source, destination) in _sourcesToDestinations(options).pairs) {
39+
var sourcesToDestinations = _sourcesToDestinations(options);
40+
for (var source in sourcesToDestinations.keys) {
4541
graph.addCanonical(
4642
FilesystemImporter('.'), p.toUri(canonicalize(source)), p.toUri(source),
4743
recanonicalize: false);
48-
var success = await watcher.compile(source, destination, ifModified: true);
49-
if (!success && options.stopOnError) {
50-
dirWatcher.events.listen(null).cancel();
51-
return;
52-
}
44+
}
45+
var success = await compileStylesheets(options, graph, sourcesToDestinations,
46+
ifModified: true);
47+
if (!success && options.stopOnError) {
48+
dirWatcher.events.listen(null).cancel();
49+
return;
5350
}
5451

5552
print("Sass is watching for changes. Press Ctrl-C to stop.\n");
@@ -67,34 +64,6 @@ final class _Watcher {
6764

6865
_Watcher(this._options, this._graph);
6966

70-
/// Compiles the stylesheet at [source] to [destination], and prints any
71-
/// errors that occur.
72-
///
73-
/// Returns whether or not compilation succeeded.
74-
Future<bool> compile(String source, String destination,
75-
{bool ifModified = false}) async {
76-
try {
77-
await compileStylesheet(_options, _graph, source, destination,
78-
ifModified: ifModified);
79-
return true;
80-
} on SassException catch (error, stackTrace) {
81-
if (!_options.emitErrorCss) _delete(destination);
82-
_printError(
83-
error.toString(color: _options.color), getTrace(error) ?? stackTrace);
84-
exitCode = 65;
85-
return false;
86-
} on FileSystemException catch (error, stackTrace) {
87-
var path = error.path;
88-
_printError(
89-
path == null
90-
? error.message
91-
: "Error reading ${p.relative(path)}: ${error.message}.",
92-
getTrace(error) ?? stackTrace);
93-
exitCode = 66;
94-
return false;
95-
}
96-
}
97-
9867
/// Deletes the file at [path] and prints a message about it.
9968
void _delete(String path) {
10069
try {
@@ -109,21 +78,6 @@ final class _Watcher {
10978
}
11079
}
11180

112-
/// Prints [message] to standard error, with [stackTrace] if [_options.trace]
113-
/// is set.
114-
void _printError(String message, StackTrace stackTrace) {
115-
var buffer = StringBuffer(message);
116-
117-
if (_options.trace) {
118-
buffer.writeln();
119-
buffer.writeln();
120-
buffer.write(Trace.from(stackTrace).terse.toString().trimRight());
121-
}
122-
123-
if (!_options.stopOnError) buffer.writeln();
124-
printError(buffer);
125-
}
126-
12781
/// Listens to `watcher.events` and updates the filesystem accordingly.
12882
///
12983
/// Returns a future that will only complete if an unexpected error occurs.
@@ -172,8 +126,9 @@ final class _Watcher {
172126
/// Returns whether all necessary recompilations succeeded.
173127
Future<bool> _handleAdd(String path) async {
174128
var destination = _destinationFor(path);
175-
176-
var success = destination == null || await compile(path, destination);
129+
var success = destination == null ||
130+
await compileStylesheets(_options, _graph, {path: destination},
131+
ifModified: true);
177132
var downstream = _graph.addCanonical(
178133
FilesystemImporter('.'), _canonicalize(path), p.toUri(path));
179134
return await _recompileDownstream(downstream) && success;
@@ -226,34 +181,42 @@ final class _Watcher {
226181
/// Returns whether all recompilations succeeded.
227182
Future<bool> _recompileDownstream(Iterable<StylesheetNode> nodes) async {
228183
var seen = <StylesheetNode>{};
229-
var toRecompile = Queue.of(nodes);
230-
231184
var allSucceeded = true;
232-
while (toRecompile.isNotEmpty) {
233-
var node = toRecompile.removeFirst();
234-
if (!seen.add(node)) continue;
185+
while (nodes.isNotEmpty) {
186+
nodes = [
187+
for (var node in nodes)
188+
if (seen.add(node)) node
189+
];
235190

236-
var success = await _compileIfEntrypoint(node.canonicalUrl);
237-
allSucceeded = allSucceeded && success;
238-
if (!success && _options.stopOnError) return false;
191+
var sourcesToDestinations = _sourceEntrypointsToDestinations(nodes);
192+
if (sourcesToDestinations.isNotEmpty) {
193+
var success = await compileStylesheets(
194+
_options, _graph, sourcesToDestinations,
195+
ifModified: true);
196+
if (!success && _options.stopOnError) return false;
239197

240-
toRecompile.addAll(node.downstream);
198+
allSucceeded = allSucceeded && success;
199+
}
200+
201+
nodes = [for (var node in nodes) ...node.downstream];
241202
}
242203
return allSucceeded;
243204
}
244205

245-
/// Compiles the stylesheet at [url] to CSS if it's an entrypoint that's being
246-
/// watched.
247-
///
248-
/// Returns `false` if compilation failed, `true` otherwise.
249-
Future<bool> _compileIfEntrypoint(Uri url) async {
250-
if (url.scheme != 'file') return true;
251-
252-
var source = p.fromUri(url);
253-
return switch (_destinationFor(source)) {
254-
var destination? => await compile(source, destination),
255-
_ => true
256-
};
206+
/// Returns a sourcesToDestinations mapping for nodes that are entrypoints.
207+
Map<String, String> _sourceEntrypointsToDestinations(
208+
Iterable<StylesheetNode> nodes) {
209+
var entrypoints = <String, String>{};
210+
for (var node in nodes) {
211+
var url = node.canonicalUrl;
212+
if (url.scheme != 'file') continue;
213+
214+
var source = p.fromUri(url);
215+
if (_destinationFor(source) case var destination?) {
216+
entrypoints[source] = destination;
217+
}
218+
}
219+
return entrypoints;
257220
}
258221

259222
/// If a Sass file at [source] should be compiled to CSS, returns the path to

‎lib/src/io/interface.dart

+8
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,16 @@ bool get isBrowser => throw '';
3232
/// sequences.
3333
bool get supportsAnsiEscapes => throw '';
3434

35+
/// Prints [message] (followed by a newline) to standard output or the
36+
/// equivalent.
37+
///
38+
/// This method is thread-safe.
39+
void safePrint(Object? message) => throw '';
40+
3541
/// Prints [message] (followed by a newline) to standard error or the
3642
/// equivalent.
43+
///
44+
/// This method is thread-safe.
3745
void printError(Object? message) => throw '';
3846

3947
/// Reads the file at [path] as a UTF-8 encoded string.

‎lib/src/io/js.dart

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ class FileSystemException {
2828
String toString() => "${p.prettyUri(p.toUri(path))}: $message";
2929
}
3030

31+
void safePrint(Object? message) {
32+
if (process case var process?) {
33+
process.stdout.write("${message ?? ''}\n");
34+
} else {
35+
console.log(message ?? '');
36+
}
37+
}
38+
3139
void printError(Object? message) {
3240
if (process case var process?) {
3341
process.stderr.write("${message ?? ''}\n");

‎lib/src/io/vm.dart

+14-1
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,21 @@ bool get supportsAnsiEscapes {
3737
return io.stdout.supportsAnsiEscapes;
3838
}
3939

40+
void safePrint(Object? message) {
41+
_threadSafeWriteLn(io.stdout, message);
42+
}
43+
4044
void printError(Object? message) {
41-
io.stderr.writeln(message);
45+
_threadSafeWriteLn(io.stderr, message);
46+
}
47+
48+
void _threadSafeWriteLn(io.IOSink sink, Object? message) {
49+
// This does have performance penality of copying buffer
50+
// if message is already a StringBuffer.
51+
// https://github.com/dart-lang/sdk/issues/53471.
52+
var buffer = StringBuffer(message.toString());
53+
buffer.writeln();
54+
sink.write(buffer);
4255
}
4356

4457
String readFile(String path) {

‎lib/src/parse/parser.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:string_scanner/string_scanner.dart';
99

1010
import '../exception.dart';
1111
import '../interpolation_map.dart';
12+
import '../io.dart';
1213
import '../logger.dart';
1314
import '../util/character.dart';
1415
import '../util/lazy_file_span.dart';
@@ -696,9 +697,9 @@ class Parser {
696697
@protected
697698
void debug([Object? message]) {
698699
if (message == null) {
699-
print(scanner.emptySpan.highlight(color: true));
700+
safePrint(scanner.emptySpan.highlight(color: true));
700701
} else {
701-
print(scanner.emptySpan.message(message.toString(), color: true));
702+
safePrint(scanner.emptySpan.message(message.toString(), color: true));
702703
}
703704
}
704705

‎test/cli/shared/colon_args.dart

-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
9797
await sass.shouldExit(65);
9898

9999
await d.file("out1.css", contains(message)).validate();
100-
await d.nothing("out2.css").validate();
101100
});
102101

103102
group("with a directory argument", () {

‎test/cli/shared/update.dart

+19-4
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,31 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
6666
await d.file("test2.scss", r"$var: 2; @import 'other'").create();
6767

6868
var sass = await update(["test1.scss:out1.css", "test2.scss:out2.css"]);
69-
expect(sass.stdout, emits(endsWith('Compiled test1.scss to out1.css.')));
70-
expect(sass.stdout, emits(endsWith('Compiled test2.scss to out2.css.')));
69+
expect(
70+
sass.stdout,
71+
emitsInAnyOrder([
72+
endsWith('Compiled test1.scss to out1.css.'),
73+
endsWith('Compiled test2.scss to out2.css.')
74+
]));
7175
await sass.shouldExit(0);
7276

77+
await d
78+
.file("out1.css", equalsIgnoringWhitespace("a { b: 1; }"))
79+
.validate();
80+
await d
81+
.file("out2.css", equalsIgnoringWhitespace("a { b: 2; }"))
82+
.validate();
83+
7384
await tick;
7485
await d.file("other.scss", r"x {y: $var}").create();
7586

7687
sass = await update(["test1.scss:out1.css", "test2.scss:out2.css"]);
77-
expect(sass.stdout, emits(endsWith('Compiled test1.scss to out1.css.')));
78-
expect(sass.stdout, emits(endsWith('Compiled test2.scss to out2.css.')));
88+
expect(
89+
sass.stdout,
90+
emitsInAnyOrder([
91+
endsWith('Compiled test1.scss to out1.css.'),
92+
endsWith('Compiled test2.scss to out2.css.')
93+
]));
7994
await sass.shouldExit(0);
8095

8196
await d

‎test/cli/shared/watch.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// https://opensource.org/licenses/MIT.
44

55
import 'package:path/path.dart' as p;
6+
import 'package:sass/src/io.dart';
67
import 'package:test/test.dart';
78
import 'package:test_descriptor/test_descriptor.dart' as d;
89
import 'package:test_process/test_process.dart';
@@ -29,7 +30,7 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
2930
/// Modifying a file very quickly after it was processed can go
3031
/// unrecognized, especially on Windows where filesystem operations can have
3132
/// very high delays.
32-
Future<void> tickIfPoll() => poll ? tick : Future.value();
33+
Future<void> tickIfPoll() => poll || isWindows ? tick : Future.value();
3334

3435
group("${poll ? 'with' : 'without'} --poll", () {
3536
group("when started", () {
@@ -128,7 +129,6 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
128129
await sass.shouldExit(65);
129130

130131
await d.file("out1.css", contains(message)).validate();
131-
await d.nothing("out2.css").validate();
132132
});
133133
});
134134

0 commit comments

Comments
 (0)
Please sign in to comment.