mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-02 09:37:49 +01:00
2bece76153
This should pass now that dart-lang/stream_channel#92 has landed and been released.
591 lines
21 KiB
Dart
591 lines
21 KiB
Dart
// Copyright 2019 Google Inc. Use of this source code is governed by an
|
|
// MIT-style license that can be found in the LICENSE file or at
|
|
// https://opensource.org/licenses/MIT.
|
|
|
|
@TestOn('vm')
|
|
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
import 'package:source_maps/source_maps.dart' as source_maps;
|
|
import 'package:test/test.dart';
|
|
import 'package:test_descriptor/test_descriptor.dart' as d;
|
|
|
|
import 'package:sass/src/embedded/embedded_sass.pb.dart';
|
|
import 'package:sass/src/embedded/utils.dart';
|
|
|
|
import 'embedded_process.dart';
|
|
import 'utils.dart';
|
|
|
|
void main() {
|
|
late EmbeddedProcess process;
|
|
setUp(() async {
|
|
process = await EmbeddedProcess.start();
|
|
});
|
|
|
|
group("exits upon protocol error", () {
|
|
test("caused by an empty message", () async {
|
|
process.send(InboundMessage());
|
|
await expectParseError(process, "InboundMessage.message is not set.");
|
|
await process.shouldExit(76);
|
|
});
|
|
|
|
test("caused by an unterminated compilation ID varint", () async {
|
|
process.stdin.add([1, 0x81]);
|
|
await expectParseError(
|
|
process, "Invalid compilation ID: continuation bit always set.",
|
|
compilationId: errorId);
|
|
await process.shouldExit(76);
|
|
});
|
|
|
|
test("caused by a 33-bit compilation ID varint", () async {
|
|
var varint = serializeVarint(0x100000000);
|
|
process.stdin.add([...serializeVarint(varint.length), ...varint]);
|
|
await expectParseError(
|
|
process, "Varint compilation ID was longer than 32 bits.",
|
|
compilationId: errorId);
|
|
await process.shouldExit(76);
|
|
});
|
|
|
|
test("caused by an invalid protobuf", () async {
|
|
process.stdin.add([2, 1, 0]);
|
|
await expectParseError(
|
|
process, "Protocol message contained an invalid tag (zero).",
|
|
compilationId: 1);
|
|
await process.shouldExit(76);
|
|
});
|
|
|
|
test("caused by a response to an inactive compilation", () async {
|
|
process.send(InboundMessage()
|
|
..canonicalizeResponse =
|
|
(InboundMessage_CanonicalizeResponse()..id = 1));
|
|
await expectParamsError(
|
|
process,
|
|
errorId,
|
|
"Response ID 1 doesn't match any outstanding requests in "
|
|
"compilation $defaultCompilationId.");
|
|
await process.shouldExit(76);
|
|
});
|
|
|
|
test("caused by duplicate compilation IDs", () async {
|
|
process.send(compileString("@import 'other'", importers: [
|
|
InboundMessage_CompileRequest_Importer()..importerId = 1
|
|
]));
|
|
await getCanonicalizeRequest(process);
|
|
|
|
process.send(compileString("a {b: c}"));
|
|
await expectParamsError(
|
|
process,
|
|
errorId,
|
|
"A CompileRequest with compilation ID $defaultCompilationId is "
|
|
"already active.");
|
|
await process.shouldExit(76);
|
|
});
|
|
});
|
|
|
|
test("a version response is valid", () async {
|
|
process.inbound.add((
|
|
0,
|
|
InboundMessage()
|
|
..versionRequest = (InboundMessage_VersionRequest()..id = 123)
|
|
));
|
|
var (compilationId, OutboundMessage(versionResponse: response)) =
|
|
await process.outbound.next;
|
|
expect(compilationId, equals(0));
|
|
expect(response.id, equals(123));
|
|
|
|
Version.parse(response.protocolVersion); // shouldn't throw
|
|
Version.parse(response.compilerVersion); // shouldn't throw
|
|
Version.parse(response.implementationVersion); // shouldn't throw
|
|
expect(response.implementationName, equals("Dart Sass"));
|
|
await process.close();
|
|
});
|
|
|
|
group("compiles CSS from", () {
|
|
test("an SCSS string by default", () async {
|
|
process.send(compileString("a {b: 1px + 2px}"));
|
|
await expectSuccess(process, "a { b: 3px; }");
|
|
await process.close();
|
|
});
|
|
|
|
test("an SCSS string explicitly", () async {
|
|
process.send(compileString("a {b: 1px + 2px}", syntax: Syntax.SCSS));
|
|
await expectSuccess(process, "a { b: 3px; }");
|
|
await process.close();
|
|
});
|
|
|
|
test("an indented syntax string", () async {
|
|
process.send(compileString("a\n b: 1px + 2px", syntax: Syntax.INDENTED));
|
|
await expectSuccess(process, "a { b: 3px; }");
|
|
await process.close();
|
|
});
|
|
|
|
test("a plain CSS string", () async {
|
|
process.send(compileString("a {b: c}", syntax: Syntax.CSS));
|
|
await expectSuccess(process, "a { b: c; }");
|
|
await process.close();
|
|
});
|
|
|
|
test("an absolute path", () async {
|
|
await d.file("test.scss", "a {b: 1px + 2px}").create();
|
|
|
|
process.send(InboundMessage()
|
|
..compileRequest = (InboundMessage_CompileRequest()
|
|
..path = p.absolute(d.path("test.scss"))));
|
|
await expectSuccess(process, "a { b: 3px; }");
|
|
await process.close();
|
|
});
|
|
|
|
test("a relative path", () async {
|
|
await d.file("test.scss", "a {b: 1px + 2px}").create();
|
|
|
|
process.send(InboundMessage()
|
|
..compileRequest = (InboundMessage_CompileRequest()
|
|
..path = p.relative(d.path("test.scss"))));
|
|
await expectSuccess(process, "a { b: 3px; }");
|
|
await process.close();
|
|
});
|
|
});
|
|
|
|
group("compiles CSS in", () {
|
|
test("expanded mode", () async {
|
|
process
|
|
.send(compileString("a {b: 1px + 2px}", style: OutputStyle.EXPANDED));
|
|
await expectSuccess(process, equals("a {\n b: 3px;\n}"));
|
|
await process.close();
|
|
});
|
|
|
|
test("compressed mode", () async {
|
|
process.send(
|
|
compileString("a {b: 1px + 2px}", style: OutputStyle.COMPRESSED));
|
|
await expectSuccess(process, equals("a{b:3px}"));
|
|
await process.close();
|
|
});
|
|
});
|
|
|
|
group("exits when stdin is closed", () {
|
|
test("immediately", () async {
|
|
process.stdin.close();
|
|
await process.shouldExit(0);
|
|
});
|
|
|
|
test("after compiling CSS", () async {
|
|
process.send(compileString("a {b: 1px + 2px}"));
|
|
await expectSuccess(process, equals("a {\n b: 3px;\n}"));
|
|
process.stdin.close();
|
|
await process.shouldExit(0);
|
|
});
|
|
|
|
test("while compiling CSS", () async {
|
|
process.send(compileString("a {b: foo() + 2px}", functions: [r"foo()"]));
|
|
await getFunctionCallRequest(process);
|
|
process.stdin.close();
|
|
await process.shouldExit(0);
|
|
});
|
|
});
|
|
|
|
test("handles many concurrent compilation requests", () async {
|
|
var totalRequests = 1000;
|
|
for (var i = 1; i <= totalRequests; i++) {
|
|
process.inbound
|
|
.add((i, compileString("a {b: foo() + 2px}", functions: [r"foo()"])));
|
|
}
|
|
|
|
var successes = 0;
|
|
process.outbound.rest.listen((pair) {
|
|
var (compilationId, message) = pair;
|
|
expect(compilationId,
|
|
allOf(greaterThan(0), lessThanOrEqualTo(totalRequests)));
|
|
|
|
if (message.hasFunctionCallRequest()) {
|
|
process.inbound.add((
|
|
compilationId,
|
|
InboundMessage()
|
|
..functionCallResponse = (InboundMessage_FunctionCallResponse()
|
|
..id = message.functionCallRequest.id
|
|
..success = (Value()
|
|
..number = (Value_Number()
|
|
..value = 1
|
|
..numerators.add("px"))))
|
|
));
|
|
} else if (message.hasCompileResponse()) {
|
|
var response = message.compileResponse;
|
|
expect(response.hasSuccess(), isTrue);
|
|
expect(response.success.css, equalsIgnoringWhitespace("a { b: 3px; }"));
|
|
|
|
successes++;
|
|
if (successes == totalRequests) {
|
|
process.stdin.close();
|
|
}
|
|
} else {
|
|
fail("Unexpected message ${message.toDebugString()}");
|
|
}
|
|
});
|
|
|
|
await process.shouldExit(0);
|
|
});
|
|
|
|
// Regression test for sass/dart-sass#2004
|
|
test("handles many sequential compilation requests", () async {
|
|
var totalRequests = 1000;
|
|
for (var i = 1; i <= totalRequests; i++) {
|
|
process.send(compileString("a {b: 1px + 2px}"));
|
|
await expectSuccess(process, "a { b: 3px; }");
|
|
}
|
|
await process.close();
|
|
});
|
|
|
|
test("closes gracefully with many in-flight compilations", () async {
|
|
// This should always be equal to the size of
|
|
// [IsolateDispatcher._isolatePool], since that's as many concurrent
|
|
// compilations as we can realistically have anyway.
|
|
var totalRequests = 15;
|
|
for (var i = 1; i <= totalRequests; i++) {
|
|
process.inbound
|
|
.add((i, compileString("a {b: foo() + 2px}", functions: [r"foo()"])));
|
|
}
|
|
|
|
await process.close();
|
|
});
|
|
|
|
test("doesn't include a source map by default", () async {
|
|
process.send(compileString("a {b: 1px + 2px}"));
|
|
await expectSuccess(process, "a { b: 3px; }", sourceMap: isEmpty);
|
|
await process.close();
|
|
});
|
|
|
|
test("doesn't include a source map with source_map: false", () async {
|
|
process.send(compileString("a {b: 1px + 2px}", sourceMap: false));
|
|
await expectSuccess(process, "a { b: 3px; }", sourceMap: isEmpty);
|
|
await process.close();
|
|
});
|
|
|
|
test("includes a source map if source_map is true", () async {
|
|
process.send(compileString("a {b: 1px + 2px}", sourceMap: true));
|
|
await expectSuccess(process, "a { b: 3px; }", sourceMap: (String map) {
|
|
var mapping = source_maps.parse(map);
|
|
var span = mapping.spanFor(2, 5)!;
|
|
expect(span.start.line, equals(0));
|
|
expect(span.start.column, equals(3));
|
|
expect(span.end, equals(span.start));
|
|
expect(mapping, isA<source_maps.SingleMapping>());
|
|
expect((mapping as source_maps.SingleMapping).files[0], isNull);
|
|
return true;
|
|
});
|
|
await process.close();
|
|
});
|
|
|
|
test(
|
|
"includes a source map without content if source_map is true and source_map_include_sources is false",
|
|
() async {
|
|
process.send(compileString("a {b: 1px + 2px}",
|
|
sourceMap: true, sourceMapIncludeSources: false));
|
|
await expectSuccess(process, "a { b: 3px; }", sourceMap: (String map) {
|
|
var mapping = source_maps.parse(map);
|
|
var span = mapping.spanFor(2, 5)!;
|
|
expect(span.start.line, equals(0));
|
|
expect(span.start.column, equals(3));
|
|
expect(span.end, equals(span.start));
|
|
expect(mapping, isA<source_maps.SingleMapping>());
|
|
expect((mapping as source_maps.SingleMapping).files[0], isNull);
|
|
return true;
|
|
});
|
|
await process.close();
|
|
});
|
|
|
|
test(
|
|
"includes a source map with content if source_map is true and source_map_include_sources is true",
|
|
() async {
|
|
process.send(compileString("a {b: 1px + 2px}",
|
|
sourceMap: true, sourceMapIncludeSources: true));
|
|
await expectSuccess(process, "a { b: 3px; }", sourceMap: (String map) {
|
|
var mapping = source_maps.parse(map);
|
|
var span = mapping.spanFor(2, 5)!;
|
|
expect(span.start.line, equals(0));
|
|
expect(span.start.column, equals(3));
|
|
expect(span.end, equals(span.start));
|
|
expect(mapping, isA<source_maps.SingleMapping>());
|
|
expect((mapping as source_maps.SingleMapping).files[0], isNotNull);
|
|
return true;
|
|
});
|
|
await process.close();
|
|
});
|
|
|
|
group("emits a log event", () {
|
|
group("for a @debug rule", () {
|
|
test("with correct fields", () async {
|
|
process.send(compileString("a {@debug hello}"));
|
|
|
|
var logEvent = await getLogEvent(process);
|
|
expect(logEvent.type, equals(LogEventType.DEBUG));
|
|
expect(logEvent.message, equals("hello"));
|
|
expect(logEvent.span.text, equals("@debug hello"));
|
|
expect(logEvent.span.start, equals(location(3, 0, 3)));
|
|
expect(logEvent.span.end, equals(location(15, 0, 15)));
|
|
expect(logEvent.span.context, equals("a {@debug hello}"));
|
|
expect(logEvent.stackTrace, isEmpty);
|
|
expect(logEvent.formatted, equals('-:1 DEBUG: hello\n'));
|
|
await process.kill();
|
|
});
|
|
|
|
test("formatted with terminal colors", () async {
|
|
process.send(compileString("a {@debug hello}", alertColor: true));
|
|
var logEvent = await getLogEvent(process);
|
|
expect(
|
|
logEvent.formatted, equals('-:1 \u001b[1mDebug\u001b[0m: hello\n'));
|
|
await process.kill();
|
|
});
|
|
});
|
|
|
|
group("for a @warn rule", () {
|
|
test("with correct fields", () async {
|
|
process.send(compileString("a {@warn hello}"));
|
|
|
|
var logEvent = await getLogEvent(process);
|
|
expect(logEvent.type, equals(LogEventType.WARNING));
|
|
expect(logEvent.message, equals("hello"));
|
|
expect(logEvent.span, equals(SourceSpan()));
|
|
expect(logEvent.stackTrace, equals("- 1:4 root stylesheet\n"));
|
|
expect(
|
|
logEvent.formatted,
|
|
equals('WARNING: hello\n'
|
|
' - 1:4 root stylesheet\n'));
|
|
await process.kill();
|
|
});
|
|
|
|
test("formatted with terminal colors", () async {
|
|
process.send(compileString("a {@warn hello}", alertColor: true));
|
|
var logEvent = await getLogEvent(process);
|
|
expect(
|
|
logEvent.formatted,
|
|
equals('\x1B[33m\x1B[1mWarning\x1B[0m: hello\n'
|
|
' - 1:4 root stylesheet\n'));
|
|
await process.kill();
|
|
});
|
|
|
|
test("encoded in ASCII", () async {
|
|
process.send(compileString("a {@debug a && b}", alertAscii: true));
|
|
var logEvent = await getLogEvent(process);
|
|
expect(
|
|
logEvent.formatted,
|
|
equals('WARNING on line 1, column 13: \n'
|
|
'In Sass, "&&" means two copies of the parent selector. You probably want to use "and" instead.\n'
|
|
' ,\n'
|
|
'1 | a {@debug a && b}\n'
|
|
' | ^^\n'
|
|
' \'\n'));
|
|
await process.kill();
|
|
});
|
|
});
|
|
|
|
test("for a parse-time deprecation warning", () async {
|
|
process.send(compileString("@if true {} @elseif true {}"));
|
|
|
|
var logEvent = await getLogEvent(process);
|
|
expect(logEvent.type, equals(LogEventType.DEPRECATION_WARNING));
|
|
expect(
|
|
logEvent.message,
|
|
equals(
|
|
'@elseif is deprecated and will not be supported in future Sass '
|
|
'versions.\n'
|
|
'\n'
|
|
'Recommendation: @else if'));
|
|
expect(logEvent.span.text, equals("@elseif"));
|
|
expect(logEvent.span.start, equals(location(12, 0, 12)));
|
|
expect(logEvent.span.end, equals(location(19, 0, 19)));
|
|
expect(logEvent.span.context, equals("@if true {} @elseif true {}"));
|
|
expect(logEvent.stackTrace, isEmpty);
|
|
await process.kill();
|
|
});
|
|
|
|
test("for a runtime deprecation warning", () async {
|
|
process.send(compileString("a {\$var: value !global}"));
|
|
|
|
var logEvent = await getLogEvent(process);
|
|
expect(logEvent.type, equals(LogEventType.DEPRECATION_WARNING));
|
|
expect(
|
|
logEvent.message,
|
|
equals("As of Dart Sass 2.0.0, !global assignments won't be able to "
|
|
"declare new variables.\n"
|
|
"\n"
|
|
"Recommendation: add `\$var: null` at the stylesheet root."));
|
|
expect(logEvent.span.text, equals("\$var: value !global"));
|
|
expect(logEvent.span.start, equals(location(3, 0, 3)));
|
|
expect(logEvent.span.end, equals(location(22, 0, 22)));
|
|
expect(logEvent.span.context, equals("a {\$var: value !global}"));
|
|
expect(logEvent.stackTrace, "- 1:4 root stylesheet\n");
|
|
await process.kill();
|
|
});
|
|
});
|
|
|
|
group("gracefully handles an error", () {
|
|
test("from invalid syntax", () async {
|
|
process.send(compileString("a {b: }"));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(failure.message, equals("Expected expression."));
|
|
expect(failure.span.text, isEmpty);
|
|
expect(failure.span.start, equals(location(6, 0, 6)));
|
|
expect(failure.span.end, equals(location(6, 0, 6)));
|
|
expect(failure.span.url, isEmpty);
|
|
expect(failure.span.context, equals("a {b: }"));
|
|
expect(failure.stackTrace, equals("- 1:7 root stylesheet\n"));
|
|
await process.close();
|
|
});
|
|
|
|
test("from the runtime", () async {
|
|
process.send(compileString("a {b: 1px + 1em}"));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(failure.message, equals("1px and 1em have incompatible units."));
|
|
expect(failure.span.text, "1px + 1em");
|
|
expect(failure.span.start, equals(location(6, 0, 6)));
|
|
expect(failure.span.end, equals(location(15, 0, 15)));
|
|
expect(failure.span.url, isEmpty);
|
|
expect(failure.span.context, equals("a {b: 1px + 1em}"));
|
|
expect(failure.stackTrace, equals("- 1:7 root stylesheet\n"));
|
|
await process.close();
|
|
});
|
|
|
|
test("from a missing file", () async {
|
|
process.send(InboundMessage()
|
|
..compileRequest =
|
|
(InboundMessage_CompileRequest()..path = d.path("test.scss")));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(failure.message, startsWith("Cannot open file: "));
|
|
expect(failure.message.replaceFirst("Cannot open file: ", "").trim(),
|
|
equalsPath(d.path('test.scss')));
|
|
expect(failure.span.text, equals(''));
|
|
expect(failure.span.context, equals(''));
|
|
expect(failure.span.start, equals(SourceSpan_SourceLocation()));
|
|
expect(failure.span.end, equals(SourceSpan_SourceLocation()));
|
|
expect(failure.span.url, equals(p.toUri(d.path('test.scss')).toString()));
|
|
expect(failure.stackTrace, isEmpty);
|
|
await process.close();
|
|
});
|
|
|
|
test("with a multi-line source span", () async {
|
|
process.send(compileString("""
|
|
a {
|
|
b: 1px +
|
|
1em;
|
|
}
|
|
"""));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(failure.span.text, "1px +\n 1em");
|
|
expect(failure.span.start, equals(location(9, 1, 5)));
|
|
expect(failure.span.end, equals(location(23, 2, 8)));
|
|
expect(failure.span.url, isEmpty);
|
|
expect(failure.span.context, equals(" b: 1px +\n 1em;\n"));
|
|
expect(failure.stackTrace, equals("- 2:6 root stylesheet\n"));
|
|
await process.close();
|
|
});
|
|
|
|
test("with multiple stack trace entries", () async {
|
|
process.send(compileString("""
|
|
@function fail() {
|
|
@return 1px + 1em;
|
|
}
|
|
|
|
a {
|
|
b: fail();
|
|
}
|
|
"""));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(
|
|
failure.stackTrace,
|
|
equals("- 2:11 fail()\n"
|
|
"- 6:6 root stylesheet\n"));
|
|
await process.close();
|
|
});
|
|
|
|
group("and includes the URL from", () {
|
|
test("a string input", () async {
|
|
process.send(compileString("a {b: 1px + 1em}", url: "foo://bar/baz"));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(failure.span.url, equals("foo://bar/baz"));
|
|
expect(
|
|
failure.stackTrace, equals("foo://bar/baz 1:7 root stylesheet\n"));
|
|
await process.close();
|
|
});
|
|
|
|
test("a path input", () async {
|
|
await d.file("test.scss", "a {b: 1px + 1em}").create();
|
|
var path = d.path("test.scss");
|
|
process.send(InboundMessage()
|
|
..compileRequest = (InboundMessage_CompileRequest()..path = path));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(p.fromUri(failure.span.url), equalsPath(path));
|
|
expect(failure.stackTrace, endsWith(" 1:7 root stylesheet\n"));
|
|
expect(failure.stackTrace.split(" ").first, equalsPath(path));
|
|
await process.close();
|
|
});
|
|
});
|
|
|
|
test("caused by using Sass features in CSS", () async {
|
|
process.send(compileString("a {b: 1px + 2px}", syntax: Syntax.CSS));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(failure.message, equals("Operators aren't allowed in plain CSS."));
|
|
expect(failure.span.text, "+");
|
|
expect(failure.span.start, equals(location(10, 0, 10)));
|
|
expect(failure.span.end, equals(location(11, 0, 11)));
|
|
expect(failure.span.url, isEmpty);
|
|
expect(failure.span.context, equals("a {b: 1px + 2px}"));
|
|
expect(failure.stackTrace, equals("- 1:11 root stylesheet\n"));
|
|
await process.close();
|
|
});
|
|
|
|
group("and provides a formatted", () {
|
|
test("message", () async {
|
|
process.send(compileString("a {b: 1px + 1em}"));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(
|
|
failure.formatted,
|
|
equals('Error: 1px and 1em have incompatible units.\n'
|
|
' ╷\n'
|
|
'1 │ a {b: 1px + 1em}\n'
|
|
' │ ^^^^^^^^^\n'
|
|
' ╵\n'
|
|
' - 1:7 root stylesheet'));
|
|
await process.close();
|
|
});
|
|
|
|
test("message with terminal colors", () async {
|
|
process.send(compileString("a {b: 1px + 1em}", alertColor: true));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(
|
|
failure.formatted,
|
|
equals('Error: 1px and 1em have incompatible units.\n'
|
|
'\x1B[34m ╷\x1B[0m\n'
|
|
'\x1B[34m1 │\x1B[0m a {b: \x1B[31m1px + 1em\x1B[0m}\n'
|
|
'\x1B[34m │\x1B[0m \x1B[31m ^^^^^^^^^\x1B[0m\n'
|
|
'\x1B[34m ╵\x1B[0m\n'
|
|
' - 1:7 root stylesheet'));
|
|
await process.close();
|
|
});
|
|
|
|
test("message with ASCII encoding", () async {
|
|
process.send(compileString("a {b: 1px + 1em}", alertAscii: true));
|
|
|
|
var failure = await getCompileFailure(process);
|
|
expect(
|
|
failure.formatted,
|
|
equals('Error: 1px and 1em have incompatible units.\n'
|
|
' ,\n'
|
|
'1 | a {b: 1px + 1em}\n'
|
|
' | ^^^^^^^^^\n'
|
|
' \'\n'
|
|
' - 1:7 root stylesheet'));
|
|
await process.close();
|
|
});
|
|
});
|
|
});
|
|
}
|