content: // 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')
  library;
  
  import 'dart:async';
  import 'dart:convert';
  import 'dart:io';
  
  import 'package:async/async.dart';
  import 'package:cli_pkg/testing.dart' as pkg;
  import 'package:test/test.dart';
  
  import 'package:sass/src/embedded/embedded_sass.pb.dart';
  import 'package:sass/src/embedded/utils.dart';
  import 'package:sass/src/embedded/util/length_delimited_transformer.dart';
  
  import 'utils.dart';
  
  /// A wrapper for [Process] that provides a convenient API for testing the
  /// embedded Sass process.
  ///
  /// If the test fails, this will automatically print out any stderr and protocol
  /// buffers from the process to aid debugging.
  ///
  /// This API is based on the `test_process` package.
  class EmbeddedProcess {
    /// The underlying process.
    final Process _process;
  
    /// A [StreamQueue] that emits each outbound protocol buffer from the process.
    ///
    /// The initial int is the compilation ID.
    StreamQueue<(int, OutboundMessage)> get outbound => _outbound;
    late StreamQueue<(int, OutboundMessage)> _outbound;
  
    /// A [StreamQueue] that emits each line of stderr from the process.
    StreamQueue<String> get stderr => _stderr;
    late StreamQueue<String> _stderr;
  
    /// A splitter that can emit new copies of [outbound].
    final StreamSplitter<(int, OutboundMessage)> _outboundSplitter;
  
    /// A splitter that can emit new copies of [stderr].
    final StreamSplitter<String> _stderrSplitter;
  
    /// A sink into which inbound messages can be passed to the process.
    ///
    /// The initial int is the compilation ID.
    final Sink<(int, InboundMessage)> inbound;
  
    /// The raw standard input byte sink.
    IOSink get stdin => _process.stdin;
  
    /// A log that includes lines from [stderr] and human-friendly serializations
    /// of protocol buffers from [outbound]
    final _log = <String>[];
  
    /// Whether [_log] has been passed to [printOnFailure] yet.
    var _loggedOutput = false;
  
    /// Returns a [Future] which completes to the exit code of the process, once
    /// it completes.
    Future<int> get exitCode => _process.exitCode;
  
    /// The process ID of the process.
    int get pid => _process.pid;
  
    /// Completes to [_process]'s exit code if it's exited, otherwise completes to
    /// `null` immediately.
    Future<int?> get _exitCodeOrNull async {
      var exitCode = await this.exitCode.timeout(
            Duration.zero,
            onTimeout: () => -1,
          );
      return exitCode == -1 ? null : exitCode;
    }
  
    /// Starts a process.
    ///
    /// [executable], [workingDirectory], [environment],
    /// [includeParentEnvironment], and [runInShell] have the same meaning as for
    /// [Process.start].
    ///
    /// If [forwardOutput] is `true`, the process's [outbound] messages and
    /// [stderr] will be printed to the console as they appear. This is only
    /// intended to be set temporarily to help when debugging test failures.
    static Future<EmbeddedProcess> start({
      String? workingDirectory,
      Map<String, String>? environment,
      bool includeParentEnvironment = true,
      bool runInShell = false,
      bool forwardOutput = false,
    }) async {
      var process = await Process.start(
        pkg.executableRunner("sass"),
        [...pkg.executableArgs("sass"), "--embedded"],
        workingDirectory: workingDirectory,
        environment: environment,
        includeParentEnvironment: includeParentEnvironment,
        runInShell: runInShell,
      );
  
      return EmbeddedProcess._(process, forwardOutput: forwardOutput);
    }
  
    /// Creates a [EmbeddedProcess] for [process].
    ///
    /// The [forwardOutput] argument is the same as that to [start].
    EmbeddedProcess._(Process process, {bool forwardOutput = false})
        : _process = process,
          _outboundSplitter = StreamSplitter(
            process.stdout.transform(lengthDelimitedDecoder).map((packet) {
              var (compilationId, buffer) = parsePacket(packet);
              return (compilationId, OutboundMessage.fromBuffer(buffer));
            }),
          ),
          _stderrSplitter = StreamSplitter(
            process.stderr
                .transform(utf8.decoder)
                .transform(const LineSplitter()),
          ),
          inbound = StreamSinkTransformer<(int, InboundMessage),
              List<int>>.fromHandlers(
            handleData: (pair, sink) {
              var (compilationId, message) = pair;
              sink.add(serializePacket(compilationId, message));
            },
          ).bind(
            StreamSinkTransformer.fromStreamTransformer(
              lengthDelimitedEncoder,
            ).bind(process.stdin),
          ) {
      addTearDown(_tearDown);
      expect(
        _process.exitCode.then((_) => _logOutput()),
        completes,
        reason: "Process `sass --embedded` never exited.",
      );
  
      _outbound = StreamQueue(_outboundSplitter.split());
      _stderr = StreamQueue(_stderrSplitter.split());
  
      _outboundSplitter.split().listen((pair) {
        for (var line in pair.$2.toDebugString().split("\n")) {
          if (forwardOutput) print(line);
          _log.add("    $line");
        }
      });
  
      _stderrSplitter.split().listen((line) {
        if (forwardOutput) print(line);
        _log.add("[e] $line");
      });
    }
  
    /// A callback that's run when the test completes.
    Future<void> _tearDown() async {
      // If the process is already dead, do nothing.
      if (await _exitCodeOrNull != null) return;
  
      _process.kill(ProcessSignal.sigkill);
  
      // Log output now rather than waiting for the exitCode callback so that
      // it's visible even if we time out waiting for the process to die.
      await _logOutput();
    }
  
    /// Formats the contents of [_log] and passes them to [printOnFailure].
    Future<void> _logOutput() async {
      if (_loggedOutput) return;
      _loggedOutput = true;
  
      var exitCodeOrNull = await _exitCodeOrNull;
  
      // Wait a timer tick to ensure that all available lines have been flushed to
      // [_log].
      await Future<void>.delayed(Duration.zero);
  
      var buffer = StringBuffer();
      buffer.write("Process `dart_sass_embedded` ");
      if (exitCodeOrNull == null) {
        buffer.write("was killed with SIGKILL in a tear-down.");
      } else {
        buffer.write("exited with exitCode $exitCodeOrNull.");
      }
      buffer.writeln(" Output:");
      buffer.writeln(_log.join("\n"));
  
      printOnFailure(buffer.toString());
    }
  
    /// Sends [message] to the process with the default compilation ID.
    void send(InboundMessage message) =>
        inbound.add((defaultCompilationId, message));
  
    /// Fetches the next message from [outbound] and asserts that it has the
    /// default compilation ID.
    Future<OutboundMessage> receive() async {
      var (actualCompilationId, message) = await outbound.next;
      expect(
        actualCompilationId,
        equals(defaultCompilationId),
        reason: "Expected default compilation ID",
      );
      return message;
    }
  
    /// Closes the process's stdin and waits for it to exit gracefully.
    Future<void> close() async {
      stdin.close();
      await shouldExit(0);
    }
  
    /// Kills the process (with SIGKILL on POSIX operating systems), and returns a
    /// future that completes once it's dead.
    ///
    /// If this is called after the process is already dead, it does nothing.
    Future<void> kill() async {
      _process.kill(ProcessSignal.sigkill);
      await exitCode;
    }
  
    /// Waits for the process to exit, and verifies that the exit code matches
    /// [expectedExitCode] (if given).
    ///
    /// If this is called after the process is already dead, it verifies its
    /// existing exit code.
    Future<void> shouldExit([int? expectedExitCode]) async {
      var exitCode = await this.exitCode;
      if (expectedExitCode == null) return;
      expect(
        exitCode,
        expectedExitCode,
        reason: "Process `dart_sass_embedded` had an unexpected exit code.",
      );
    }
  }
  