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.
  
  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_embedded/src/embedded_sass.pb.dart';
  import 'package:sass_embedded/src/util/length_delimited_transformer.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.
    StreamQueue<OutboundMessage> get outbound => _outbound;
    late StreamQueue<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<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.
    final Sink<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("dart-sass-embedded"),
          pkg.executableArgs("dart-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((message) => OutboundMessage.fromBuffer(message))),
          _stderrSplitter = StreamSplitter(process.stderr
              .transform(utf8.decoder)
              .transform(const LineSplitter())),
          inbound = StreamSinkTransformer<InboundMessage, List<int>>.fromHandlers(
              handleData: (message, sink) =>
                  sink.add(message.writeToBuffer())).bind(
              StreamSinkTransformer.fromStreamTransformer(lengthDelimitedEncoder)
                  .bind(process.stdin)) {
      addTearDown(_tearDown);
      expect(_process.exitCode.then((_) => _logOutput()), completes,
          reason: "Process `dart_sass_embedded` never exited.");
  
      _outbound = StreamQueue(_outboundSplitter.split());
      _stderr = StreamQueue(_stderrSplitter.split());
  
      _outboundSplitter.split().listen((message) {
        for (var line in message.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());
    }
  
    /// 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.");
    }
  }
  