#include "doctest/doctest.h"
// ^ Violates linting rules, so include first.
#include "absl/strings/match.h"
#include "common/common.h"
#include "common/sort/sort.h"
#include "test/helpers/lsp.h"
#include "test/helpers/packages.h"
#include "test/lsp/ProtocolTest.h"

using namespace std;

namespace sorbet::test::lsp {
using namespace sorbet::realmain::lsp;

namespace {
optional<SorbetTypecheckRunStatus> getTypecheckRunStatus(const LSPMessage &msg) {
    if (msg.isNotification() && msg.method() == LSPMethod::SorbetTypecheckRunInfo) {
        auto &typecheckInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(msg.asNotification().params);
        return typecheckInfo->status;
    }
    return nullopt;
}

void sortTimersByStartTime(vector<unique_ptr<CounterImpl::Timing>> &times) {
    fast_sort(times, [](const auto &a, const auto &b) -> bool { return a->start.usec < b->start.usec; });
}

void checkDiagnosticTimes(vector<unique_ptr<CounterImpl::Timing>> times, size_t expectedSize,
                          bool assertUniqueStartTimes) {
    CHECK_EQ(times.size(), expectedSize);
    sortTimersByStartTime(times);

    if (assertUniqueStartTimes) {
        // No two diagnostic latency timers should have the same start timestamp.
        for (size_t i = 1; i < times.size(); i++) {
            CHECK_LT(times[i - 1]->start.usec, times[i]->start.usec);
        }
    }
}

// The resolution of MONOTONIC_COARSE seems to be ~1ms, so we use >1ms delay to ensure unique clock values.
constexpr auto timestampGranularity = chrono::milliseconds(2);

class MultithreadedProtocolTest : public ProtocolTest {
public:
    MultithreadedProtocolTest() : ProtocolTest(/*multithreading*/ true, /*caching*/ false) {}

    // The timeout here is a balance: we want to wait long enough for all slow path edits to
    // complete, but not so long that we're just busy waiting. Because the files involved in this
    // test are generally tiny, 500ms should be plenty of time, but if this is causing flakiness we
    // will need to adjust this or, ideally, make the slow path faster ;-)
    void assertSlowPathBlockedAndUnblock(int timeout_ms = 500) {
        auto &wrapper = dynamic_cast<MultiThreadedLSPWrapper &>(*lspWrapper);
        auto msg = wrapper.read(timeout_ms);
        REQUIRE(msg == nullptr);
        setSlowPathBlocked(false);
    }
};

} // namespace

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "MultithreadedWrapperWorks") {
    assertErrorDiagnostics(initializeLSP(), {});
    {
        auto initCounters = getCounters();
        CHECK_EQ(initCounters.getCategoryCounter("lsp.messages.processed", "initialize"), 1);
        CHECK_EQ(initCounters.getCategoryCounter("lsp.messages.processed", "initialized"), 1);
        CHECK_EQ(initCounters.getCategoryCounter("lsp.updates", "slowpath"), 1);
        CHECK_EQ(initCounters.getCategoryCounterSum("lsp.updates"), 1);
        CHECK_EQ(initCounters.getCategoryCounterSum("lsp.messages.canceled"), 0);
    }

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    sendAsync(*openFile("yolo1.rb", "# typed: true\nclass Foo2\n  def branch\n    2 + \"dog\"\n  end\nend\n"));
    // Pause to differentiate message times.
    this_thread::sleep_for(timestampGranularity);
    sendAsync(*changeFile("yolo1.rb", "# typed: true\nclass Foo1\n  def branch\n    1 + \"bear\"\n  end\nend\n", 3));

    // Pause so that all latency timers for the above operations get reported.
    this_thread::sleep_for(chrono::milliseconds(2));

    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt))),
                           {{"yolo1.rb", 3, "bear"}});

    auto counters = getCounters();
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.workspaceEdit"), 1);
    CHECK_EQ(counters.getTimings("task_latency", {{"method", "sorbet.workspaceEdit"}}).size(), 1);
    CHECK_EQ(counters.getTimings("task_latency").size(), counters.getHistogramCount("task_latency"));
    CHECK_EQ(counters.getCategoryCounterSum("lsp.messages.canceled"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath"), 1);
    CHECK_EQ(counters.getCategoryCounterSum("lsp.updates"), 1);
    // 1 per edit
    checkDiagnosticTimes(counters.getTimings("last_diagnostic_latency"), 2, /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CancelsSlowPathWhenNewEditWouldTakeFastPathWithOldEdits") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create three files.
    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\n"
                                                    "\n"
                                                    "class Foo\n"
                                                    "\n"
                                                    "end\n")),
                           {});
    assertErrorDiagnostics(send(*openFile("bar.rb", "# typed: true\n"
                                                    "\n"
                                                    "class Bar\n"
                                                    "extend T::Sig\n"
                                                    "\n"
                                                    "sig{returns(String)}\n"
                                                    "def hello\n"
                                                    "\"hi\"\n"
                                                    "end\n"
                                                    "end\n")),
                           {});
    // baz calls the method defined in bar
    assertErrorDiagnostics(send(*openFile("baz.rb", "# typed: true\n"
                                                    "\n"
                                                    "class Baz\n"
                                                    "extend T::Sig\n"
                                                    "\n"
                                                    "sig{returns(String)}\n"
                                                    "def hello\n"
                                                    "Bar.new.hello\n"
                                                    "end\n"
                                                    "end\n")),
                           {});

    // clear counters
    getCounters();

    // Slow path edits two files. One introduces error.
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    // Something that will introduce slow path in foo.rb
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "\n"
                          "class Foo\n"
                          "  module Inner; end\n"
                          "end\n",
                          2, true));
    // Pause to differentiate message times
    this_thread::sleep_for(chrono::milliseconds(2));
    // Typechecking error in bar.rb
    sendAsync(*changeFile("bar.rb",
                          "# typed: true\n"
                          "\n"
                          "class Bar\n"
                          "extend T::Sig\n"
                          "\n"
                          "sig{returns(Integer)}\n" // String -> Integer
                          "def hello\n"
                          "\"hi\"\n"
                          "end\n"
                          "end\n",
                          2, true));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));
    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Pause for a moderate amount of time so that we can check that the subsequent fast path includes the latency
    // timer from the initial slow path.
    this_thread::sleep_for(chrono::milliseconds(5));

    // Make another edit that undoes the slow path in foo.rb
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "\n"
                          "class Foo\n"
                          "end\n",
                          2, false));

    // Wait for first typecheck run to get canceled.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Cancelled);
    }

    // Send a no-op to clear out the pipeline. Should have errors in bar and baz, but not foo.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {
                               {"bar.rb", 7, "Expected `Integer` but found `String(\"hi\")` for method result type"},
                               {"baz.rb", 7, "Expected `String` but found `Integer` for method result type"},
                           });

    auto counters = getCounters();
    // N.B.: lsp.messages.processed contains canceled slow paths.
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.workspaceEdit"), 2);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.mergedEdits"), 2);

    // We don't report task latencies for merged edits or canceled slow paths.
    auto taskLatency = counters.getTimings("task_latency", {{"method", "sorbet.workspaceEdit"}});
    CHECK_EQ(taskLatency.size(), 1);
    CHECK_GE(taskLatency[0]->end.usec - taskLatency[0]->start.usec, 5'000 /* 5ms */);
    CHECK_EQ(counters.getCategoryCounterSum("lsp.messages.canceled"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "fastpath"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath_canceled"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "query"), 0);

    auto lastDiagnosticLatency = counters.getTimings("last_diagnostic_latency");
    sortTimersByStartTime(lastDiagnosticLatency);
    CHECK_GE(lastDiagnosticLatency[0]->end.usec - lastDiagnosticLatency[0]->start.usec, 5'000 /* 5ms */);
    CHECK_GE(lastDiagnosticLatency[1]->end.usec - lastDiagnosticLatency[1]->start.usec, 5'000 /* 5ms */);
    // 1 per edit
    checkDiagnosticTimes(move(lastDiagnosticLatency), 3, /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CancelsSlowPathWhenNewEditWouldTakeSlowPath") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Initial state: Two empty files.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    assertErrorDiagnostics(send(*openFile("bar.rb", "")), {});

    // clear counters
    getCounters();

    // Slow path 1: Edit foo to have an error since Bar doesn't exist. Expect a cancelation.
    sendAsync(*changeFile(
        "foo.rb", "# typed: true\n\nclass Foo\nextend T::Sig\nsig{returns(Integer)}\ndef foo\nBar.new.bar\nend\nend\n",
        2, true));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    // Slow path 2: Bar defines the expected method, but declared with a non-integer return value (so foo now has a
    // new error).
    sendAsync(*changeFile("bar.rb",
                          "# typed: true\n\nclass Bar\nextend T::Sig\nsig{returns(String)}\ndef bar\n10\nend\nend\n", 2,
                          false));
    // Pause so that all latency timers for the above operations get reported.
    this_thread::sleep_for(timestampGranularity);
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Wait for first typecheck run to get canceled.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Cancelled);
    }

    // Send a no-op to clear out the pipeline. Should have one error per file.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {
                               {"foo.rb", 6, "Expected `Integer` but found `String` for method result type"},
                               {"bar.rb", 6, "Expected `String` but found `Integer(10)` for method result type"},
                           });

    auto counters = getCounters();
    // N.B.: lsp.messages.processed contains canceled slow paths.
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.workspaceEdit"), 2);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.mergedEdits"), 1);

    // We don't report task latencies for merged edits or canceled slow paths.
    CHECK_EQ(counters.getTimings("task_latency", {{"method", "sorbet.workspaceEdit"}}).size(), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "fastpath"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath_canceled"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "query"), 0);
    // 1 per edit
    checkDiagnosticTimes(counters.getTimings("last_diagnostic_latency"), 2, /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithHover") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a new file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});

    // clear counters
    getCounters();

    // Slow path: Edit foo to have a class with a documentation string.
    sendAsync(*changeFile("foo.rb", "# typed: true\n# A class that does things.\nclass Foo\nextend T::Sig\nend\n", 2,
                          false, 1));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    // Send a hover to request the documentation string.
    sendAsync(*hover("foo.rb", 2, 6));
    // Pause so that all latency timers for the above operations get reported.
    this_thread::sleep_for(chrono::milliseconds(2));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // First response should be hover.
    {
        auto response = readAsync();
        REQUIRE(response->isResponse());
        auto &hoverText =
            get<unique_ptr<Hover>>(get<variant<JSONNullObject, unique_ptr<Hover>>>(*response->asResponse().result));
        CHECK(absl::StrContains(hoverText->contents->value, "A class that does things"));
    }

    // Second should be typecheck run signaling that typechecking completed.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))), {});

    auto counters = getCounters();
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.workspaceEdit"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.mergedEdits"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "textDocument.hover"), 1);

    CHECK_EQ(counters.getTimings("task_latency", {{"method", "sorbet.workspaceEdit"}}).size(), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "fastpath"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath_canceled"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "query"), 1);
    // 1 per edit
    checkDiagnosticTimes(counters.getTimings("last_diagnostic_latency"), 1, /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithCodeAction") {
    // Note:
    //
    // VS Code sends a code action request after almost every edit, trying to figure out which
    // actions are available at the new cursor position, so this case is actually abundantly common.

    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a new file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});

    // clear counters
    getCounters();

    // Add something to the empty file to trigger a slow path
    auto preemptionsExpected = 1;
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class A\n"
                          "  def example; end\n"
                          "  \n"
                          "end\n",
                          2, false, preemptionsExpected));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    // Send a codeAction request at the cursor position
    sendAsync(*codeAction("foo.rb", 3, 2));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // First response should be codeAction.
    {
        auto response = readAsync();
        REQUIRE(response->isResponse());
        auto &codeActions = get<vector<unique_ptr<CodeAction>>>(
            get<variant<JSONNullObject, vector<unique_ptr<CodeAction>>>>(*response->asResponse().result));
        CHECK_EQ(codeActions.size(), 0);
    }

    // Second should be typecheck run signaling that typechecking completed.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))), {});

    auto counters = getCounters();
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.workspaceEdit"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.mergedEdits"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "textDocument.codeAction"), 1);

    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "fastpath"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath"), 1);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "slowpath_canceled"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.updates", "query"), 1);
    // 1 per edit
    checkDiagnosticTimes(counters.getTimings("last_diagnostic_latency"), 1, /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithHoverAndReturnsErrors") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a new file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});

    // Slow path: Edit foo to have a class with a documentation string and a method with an error.
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n# A class that does things.\nclass Foo\nextend "
                          "T::Sig\nsig{returns(String)}\ndef bar\n3\nend\nend\n",
                          2, false, 1));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Send a hover to request the documentation string.
    sendAsync(*hover("foo.rb", 2, 6));

    // First response should be hover.
    {
        auto response = readAsync();
        REQUIRE(response->isResponse());
        auto &hoverText =
            get<unique_ptr<Hover>>(get<variant<JSONNullObject, unique_ptr<Hover>>>(*response->asResponse().result));
        CHECK(absl::StrContains(hoverText->contents->value, "A class that does things"));
    }

    // Send a no-op to clear out the pipeline. Should have one error in `foo.rb`.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {
                               {"foo.rb", 6, "Expected `String` but found `Integer(3)` for method result type"},
                           });
    checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 2,
                         /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithFastPath") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create two new files.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    assertErrorDiagnostics(send(*openFile("bar.rb", "")), {});

    // Slow path: Edit foo to have a class with two methods and two errors, and add an error to bar.
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\nclass Foo\nextend T::Sig\nsig{returns(Integer)}\ndef "
                          "bar\nbaz\nend\nsig{returns(Float)}\ndef baz\n'not a float'\nend\nend\n",
                          2, false, 1));
    sendAsync(*changeFile(
        "bar.rb", "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(String)}\ndef branch\n1\nend\nend\n", 3));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path 1: Correct _one_ error.
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\nclass Foo\nextend T::Sig\nsig{returns(Integer)}\ndef "
                          "bar\n10\nend\nsig{returns(Float)}\ndef baz\n'not a float'\nend\nend\n",
                          3));

    // Send a no-op to clear out the pipeline. Should have two error now: bar.rb from slow path and foo.rb from fast
    // path.
    assertErrorDiagnostics(
        send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
        {
            {"foo.rb", 9, "Expected `Float` but found `String(\"not a float\")` for method result type"},
            {"bar.rb", 5, "Expected `String` but found `Integer(1)` for method result type"},
        });
    checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 5,
                         /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithFastPathThatFixesAllErrors") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create two new files.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    assertErrorDiagnostics(send(*openFile("bar.rb", "")), {});

    // Slow path: Edit foo to have a class with an error that also causes an error in bar
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  extend T::Sig\n"
                          "  sig{returns(Integer)}\n"
                          "  def bar\n"
                          "    'hello'\n"
                          "  end\n"
                          "end\n",
                          2, false, 1));
    sendAsync(*changeFile("bar.rb",
                          "# typed: true\n"
                          "class Bar\n"
                          "  extend T::Sig\n"
                          "  sig{returns(String)}\n"
                          "  def str\n"
                          "    Foo.new.bar\n"
                          "  end\n"
                          "end\n",
                          3));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path 1: Correct return type on Foo#bar, which should fix foo.rb and bar.rb.
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  extend T::Sig\n"
                          "  sig{returns(String)}\n"
                          "  def bar\n"
                          "    'hello'\n"
                          "  end\n"
                          "end\n",
                          3));
    // Send a no-op to clear out the pipeline. Should have no errors at end of both typechecking runs.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))), {});

    // Need to re-evaluate the test strategy for timings
    // checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 5,
    //                      /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithFastPathAndThenCancelBoth") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create three new files! foo.rb defines a class, bar.rb defines a class and method used in baz.rb.
    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\nend")), {});
    assertErrorDiagnostics(
        send(*openFile("bar.rb",
                       "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(String)}\ndef str\n'hi'\nend\nend\n")),
        {});
    assertErrorDiagnostics(
        send(*openFile(
            "baz.rb",
            "# typed: true\nclass Baz\nextend T::Sig\nsig{returns(String)}\ndef bar\nBar.new.str\nend\nend\n")),
        {});

    // Slow path: foo.rb will have a syntax error
    sendAsync(*changeFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\n", 2, true, 1));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path [preempt]: Change return type in bar.rb, which indirectly causes an error in baz.rb too.
    sendAsync(*changeFile(
        "bar.rb", "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(Integer)}\ndef str\n'hi'\nend\nend\n", 4));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path [cancel]: Fix syntax error. Foo should not have any errors.
    assertErrorDiagnostics(send(*changeFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\nend", 4)),
                           {
                               {"bar.rb", 5, "Expected `Integer` but found `String(\"hi\")` for method result type"},
                               {"baz.rb", 5, "Expected `String` but found `Integer` for method result type"},
                           });

    auto counters = getCounters();
    checkDiagnosticTimes(counters.getTimings("last_diagnostic_latency"), 6, /* assertUniqueStartTimes */ false);

    // N.B.: This counter contains canceled edits.
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.workspaceEdit"), 6);
    // mergedEdits should not count the preempted fast path, but should count the canceled slow path.
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "sorbet.mergedEdits"), 1);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptSlowPathWithFastPathAndBothErrorsAreReported") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create three new files! foo.rb defines a class, bar.rb defines a class and method used in baz.rb.
    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\nend")), {});
    assertErrorDiagnostics(
        send(*openFile("bar.rb",
                       "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(String)}\ndef str\n'hi'\nend\nend\n")),
        {});
    assertErrorDiagnostics(
        send(*openFile(
            "baz.rb",
            "# typed: true\nclass Baz\nextend T::Sig\nsig{returns(String)}\ndef bar\nBar.new.str\nend\nend\n")),
        {});

    // Slow path: foo.rb will have a syntax error
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "extend(T::Sig",
                          2, false, 1));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path [preempt]: Change return type in bar.rb, which indirectly causes an error in baz.rb too.
    sendAsync(*changeFile(
        "bar.rb", "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(Integer)}\ndef str\n'hi'\nend\nend\n", 4));

    // We should receive errors for `foo`, `bar`, and `baz`.
    // Send a no-op to clear out the pipeline.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {
                               {"foo.rb", 2, "unexpected token"},
                               {"bar.rb", 5, "Expected `Integer` but found `String(\"hi\")` for method result type"},
                               {"baz.rb", 5, "Expected `String` but found `Integer` for method result type"},
                           });
    checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 5,
                         /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanCancelSlowPathWithFastPathThatReintroducesOldError") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // foo stands alone
    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\nend\n")), {});
    // bar defines method
    assertErrorDiagnostics(
        send(*openFile("bar.rb",
                       "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(Integer)}\ndef str\n10\nend\nend\n")),
        {});
    // baz uses it
    assertErrorDiagnostics(
        send(*openFile(
            "baz.rb",
            "# typed: true\nclass Baz\nextend T::Sig\nsig{returns(String)}\ndef bar\nBar.new.str\nend\nend\n")),
        {{"baz.rb", 5, "Expected `String` but found `Integer` for method result type"}});

    // Slow path: Introduce syntax error to foo.rb and change method sig in bar.rb to fix error in baz.rb
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    sendAsync(*changeFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\n", 2, true));
    sendAsync(*changeFile(
        "bar.rb", "# typed: true\n\nclass Bar\nextend T::Sig\n\nsig{returns(String)}\ndef str\n'10'\nend\nend\n", 2,
        true));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path: Undo previous changes. Should re-introduce error on baz.rb.
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    sendAsync(*changeFile("foo.rb", "# typed: true\nclass Foo\nextend T::Sig\nend\n", 3));
    sendAsync(*changeFile(
        "bar.rb", "# typed: true\n\nclass Bar\nextend T::Sig\n\nsig{returns(Integer)}\ndef str\n10\nend\nend\n", 3));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Send a no-op to clear out the pipeline. Should have one error on baz.rb.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {{"baz.rb", 5, "Expected `String` but found `Integer` for method result type"}});
    checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 7,
                         /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanCancelSlowPathEvenIfAddsFile") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // bar has no error
    assertErrorDiagnostics(
        send(*openFile("bar.rb",
                       "# typed: true\nclass Bar\nextend T::Sig\nsig{returns(Integer)}\ndef hi\n10\nend\nend\n")),
        {});

    // Slow path: New file, no error
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    sendAsync(*openFile("foo.rb", ""));
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\nclass Foo\nextend T::Sig\nsig {returns(Integer)}\ndef foo\n10\nend\nend\n", 2,
                          true, 1));

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Introduce error fast path: Will preempt.
    sendAsync(*changeFile(
        "foo.rb", "# typed: true\nclass Foo\nextend T::Sig\nsig {returns(Integer)}\ndef foo\n'hi'\nend\nend\n", 2));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Introduce slow path in unrelated file; will cancel.
    sendAsync(*openFile("bar.rb", "# typed: true\n"
                                  "class Bar\n"
                                  "extend T::Sig\n"
                                  "sig{returns(Integer)\n"
                                  "def hi\n"
                                  "10\n"
                                  "end"));

    // Send fence to clear out the pipeline.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {{"foo.rb", 5, "Expected `Integer` but found `String(\"hi\")` for method result type"},
                            {"bar.rb", 1, "Hint: this \"class\" token is not closed before the end of the file"},
                            {"bar.rb", 6, "unexpected"}});
    checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 5,
                         /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "SlowFooThenFastBarThenUndoSlowFoo") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\n"
                                                    "class Foo\n"
                                                    "end\n")),
                           {});
    assertErrorDiagnostics(send(*openFile("bar.rb", "# typed: true\n"
                                                    "class Bar\n"
                                                    "  extend T::Sig\n"
                                                    "  sig {returns(Integer)}\n"
                                                    "  def example\n"
                                                    "    1+1\n"
                                                    "  end\n"
                                                    "end\n")),
                           {});

    // Slow path in foo.rb
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class FooNew\n"
                          "end\n",
                          2, true));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    // Fast path edit in bar.rb preempts previous foo.rb edit
    sendAsync(*changeFile("bar.rb",
                          "# typed: true\n"
                          "class Bar\n"
                          "  extend T::Sig\n"
                          "  sig {returns(String)}\n"
                          "  def example\n"
                          "    1+1\n"
                          "  end\n"
                          "end\n",
                          2, false));
    // Undo change in foo.rb
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "end\n",
                          3));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));

    // Send a no-op to clear out the pipeline. Should have one error on baz.rb.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))),
                           {{"bar.rb", 5, "Expected `String` but found `Integer` for method result type"}});
}
TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanceledRequestsDontReportLatencyMetrics") {
    assertErrorDiagnostics(initializeLSP(), {});

    // Create a new file.
    assertErrorDiagnostics(
        send(*openFile("foo.rb", "# typed: true\n# A class that does things.\nclass Foo\nextend T::Sig\nend\n")), {});

    // clear counters
    getCounters();

    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::PAUSE, nullopt)));
    // Send a hover to request the documentation string.
    sendAsync(*hover("foo.rb", 2, 6));
    // Pause so that all latency timer for the hover is >1ms and would normally get reported.
    this_thread::sleep_for(chrono::milliseconds(2));
    // Cancel the hover.
    sendAsync(*cancelRequest(nextId - 1));
    sendAsync(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::RESUME, nullopt)));
    // Clear out the pipeline to wait for previous requests to finish.
    send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20)));

    auto counters = getCounters();
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.processed", "textDocument.hover"), 0);
    CHECK_EQ(counters.getCategoryCounter("lsp.messages.canceled", "textDocument.hover"), 1);
    CHECK_EQ(counters.getTimings("task_latency", {{"method", "textDocument.hover"}}).size(), 0);
    checkDiagnosticTimes(getCounters().getTimings("last_diagnostic_latency"), 0,
                         /* assertUniqueStartTimes */ false);
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "ErrorIntroducedInSlowPathPreemptionByFastPathClearedByNewSlowPath") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create new file
    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\n"
                                                    "module M\n"
                                                    "end\n"
                                                    "class Foo\n"
                                                    "end")),
                           {});

    // Slow path: no errors
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "module M\n"
                          "end\n"
                          "class Foo\n"
                          "  extend M\n"
                          "end",
                          2, true, 1));

    // Wait for typechecking to begin to avoid races.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path [preempt]: Introduce an error with an edit that could take fast path
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "module M\n"
                          "end\n"
                          "class Foo\n"
                          "  extend M\n"
                          "  self.method_on_m()\n"
                          "end",
                          3));

    // Wait for typechecking of fast path so it and subsequent slow path change aren't combined
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Slow path: error will be resolved
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "module M\n"
                          "  def method_on_m; end\n"
                          "end\n"
                          "class Foo\n"
                          "  include M\n" // Force slow path
                          "  extend M\n"
                          "  self.method_on_m()\n"
                          "end",
                          4, false, 0));

    // Send a no-op to clear out the pipeline.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))), {});
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "StallInSlowPathWorks") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a simple file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  def foo\n"
                          "  end\n"
                          "end\n",
                          2));

    // Wait for initial typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Wait for initial typechecking to finish.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Turn on slow path blocking, and send an edit that is going to cause a slow path.
    setSlowPathBlocked(true);
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Bar\n"
                          "  def foo\n"
                          "  end\n"
                          "end\n",
                          3, false, 0));

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    assertSlowPathBlockedAndUnblock();

    // The slow path should now be unblocked. Wait for typechecking to end.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "PreemptionCanceledForSlowPathWorks") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a simple file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "end\n",
                          2));

    // Wait for initial typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Wait for initial typechecking to finish.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Clear counters
    getCounters();

    // Turn on slow path blocking, and send an edit that is going to cause a slow path.
    setSlowPathBlocked(true);
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Mod\n"
                          "end\n",
                          3, false, 0));

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Make a completion query in the queue so that we schedule preemption.
    sendAsync(*completion("foo.rb", 4, 14));

    // Send another update that finishes the include. This should cancel preemption
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include ModuleA\n"
                          "end\n",
                          4, false, 0));

    assertSlowPathBlockedAndUnblock();

    // The slow path should now be unblocked, and will be canceled immediately.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Cancelled);
    }

    // Now we'll see the second slow path edit start
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Then the response to the completion request
    {
        auto msg = readAsync();
        REQUIRE(msg->isResponse());
        REQUIRE_EQ(msg->asResponse().requestMethod, LSPMethod::TextDocumentCompletion);
    }

    // And finally the slow path finishes
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Verify the path that we took during cancellation.
    {
        auto counters = getCounters();
        CHECK_EQ(counters.getCategoryCounter("lsp.preemption.cancelation", "single"), 1);
        CHECK_EQ(counters.getCategoryCounter("lsp.preemption.cancelation", "merged"), 0);
    }

    // Make another slow path edit that deletes part of the module name
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Modu\n"
                          "end\n",
                          5, false, 0));

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // An error will be raised for the include argument
    {
        vector<unique_ptr<LSPMessage>> msgs;
        msgs.emplace_back(readAsync());
        // TODO(trevor): why does this report the same error twice?
        assertErrorDiagnostics(move(msgs), {{"foo.rb", 4, "Unable to resolve constant `Modu`"},
                                            {"foo.rb", 4, "Unable to resolve constant `Modu`"}});
    }

    // And finally the slow path finishes
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "PreemptionCanceledForSlowPathWorksWithDelete") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a simple file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Foo::Bar::Baz::Foo\n"
                          "end\n",
                          2));

    // Wait for initial typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Wait for initial typechecking to finish, with errors.
    {
        auto msg = readAsync();
        REQUIRE(msg->isNotification());
        REQUIRE_EQ(msg->asNotification().method, LSPMethod::TextDocumentPublishDiagnostics);

        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Clear counters
    getCounters();

    // Turn on slow path blocking, and send an edit that is going to cause a slow path.
    setSlowPathBlocked(true);
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Mod\n"
                          "end\n",
                          3, false, 0));

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Make a completion query at the end of the old include into the queue so that we schedule preemption.
    sendAsync(*completion("foo.rb", 4, 28));

    // Send another update that finishes the include. This should cancel preemption
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include ModuleA\n"
                          "end\n",
                          4, false, 0));

    assertSlowPathBlockedAndUnblock();

    // The slow path should now be unblocked, and will be canceled immediately.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Cancelled);
    }

    // Now we'll see the second slow path edit start
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Then the response to the completion request
    {
        auto msg = readAsync();
        REQUIRE(msg->isResponse());
        REQUIRE_EQ(msg->asResponse().requestMethod, LSPMethod::TextDocumentCompletion);
    }

    // And finally the slow path finishes after reporting that there are no more errors.
    {
        std::vector<unique_ptr<LSPMessage>> msgs;
        msgs.emplace_back(readAsync());
        assertErrorDiagnostics(move(msgs), {});

        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Verify the path that we took during cancellation.
    {
        auto counters = getCounters();
        CHECK_EQ(counters.getCategoryCounter("lsp.preemption.cancelation", "single"), 1);
        CHECK_EQ(counters.getCategoryCounter("lsp.preemption.cancelation", "merged"), 0);
    }

    // Make another slow path edit that deletes part of the module name
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Modu\n"
                          "end\n",
                          5, false, 0));

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // An error will be raised for the include argument
    {
        vector<unique_ptr<LSPMessage>> msgs;
        msgs.emplace_back(readAsync());
        // TODO(trevor): why does this report the same error twice?
        assertErrorDiagnostics(move(msgs), {{"foo.rb", 4, "Unable to resolve constant `Modu`"},
                                            {"foo.rb", 4, "Unable to resolve constant `Modu`"}});
    }

    // And finally the slow path finishes
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "MergingEditsWhenCancellingPreemptionWorks") {
    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Create a simple file.
    assertErrorDiagnostics(send(*openFile("foo.rb", "")), {});
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "\n"
                          "  def foo\n"
                          "  end\n"
                          "end\n",
                          2));

    // Wait for initial typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Wait for initial typechecking to finish.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Clear counters
    getCounters();

    // Turn on slow path blocking, and send an edit that is going to cause a slow path.
    setSlowPathBlocked(true);
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Mod\n"
                          "  def foo\n"
                          "  end\n"
                          "end\n",
                          3, false, 0));

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Send another change that would take the fast path
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include Mod\n"
                          "  def foo\n"
                          "    puts :hi\n"
                          "  end\n"
                          "end\n",
                          4, false, 0));

    // Make a completion query in the queue so that we schedule preemption.
    sendAsync(*completion("foo.rb", 4, 14));

    // Send another update that finishes the include. This should cancel preemption
    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo\n"
                          "  module ModuleA; end\n"
                          "  module ModuleB; end\n"
                          "  include ModuleA\n"
                          "  def foo\n"
                          "    puts :hi\n"
                          "  end\n"
                          "end\n",
                          5, false, 0));

    assertSlowPathBlockedAndUnblock();

    // The slow path should now be unblocked, and will be canceled immediately.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Cancelled);
    }

    // Now we'll see the second slow path edit start
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Then the response to the completion request
    {
        auto msg = readAsync();
        REQUIRE(msg->isResponse());
        REQUIRE_EQ(msg->asResponse().requestMethod, LSPMethod::TextDocumentCompletion);
    }

    // And finally the slow path finishes
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Ended);
    }

    // Verify the path that we took during cancellation.
    {
        auto counters = getCounters();
        CHECK_EQ(counters.getCategoryCounter("lsp.preemption.cancelation", "single"), 0);
        CHECK_EQ(counters.getCategoryCounter("lsp.preemption.cancelation", "merged"), 1);
    }
}

TEST_CASE_FIXTURE(MultithreadedProtocolTest, "CanPreemptDoesNotCrashOnEvictedFilesShortCircuit") {
    // Set lspMaxFilesOnFastPath to 1 so the short-circuit in fastPathFilesToTypecheck
    // triggers with just a few extra files: totalChanged > 2 * 1 = 2.
    // (We could do this without, but it would involve making dozens of files that use the symbol)
    auto opts = make_shared<realmain::options::Options>();
    opts->lspMaxFilesOnFastPath = 1;
    resetState(opts);

    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Sequence of edits:
    //
    // - Slow path edit in foo.rb
    // - While slow path is going, fast path edit in bar.rb that implicates more than lspMaxFilesOnFastPath

    assertErrorDiagnostics(send(*openFile("bar.rb", "# typed: true\n"
                                                    "class Bar\n"
                                                    "  extend T::Sig\n"
                                                    "  sig { returns(String) }\n"
                                                    "  def compute_value = 'hi'\n"
                                                    "end\n")),
                           {});
    assertErrorDiagnostics(send(*openFile("baz.rb", "# typed: true\n"
                                                    "class Baz\n"
                                                    "  def baz_method = Bar.new.compute_value\n"
                                                    "end\n")),
                           {});
    assertErrorDiagnostics(send(*openFile("qux.rb", "# typed: true\n"
                                                    "class Qux\n"
                                                    "  def qux_method = Bar.new.compute_value\n"
                                                    "end\n")),
                           {});
    assertErrorDiagnostics(send(*openFile("foo.rb", "# typed: true\n"
                                                    "class Foo\n"
                                                    "end")),
                           {});

    setSlowPathBlocked(true);

    sendAsync(*changeFile("foo.rb",
                          "# typed: true\n"
                          "class Foo2\n"
                          "end",
                          2));

    // Wait for the slow path to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    // Fast path edit changes compute_value: which means 3 files on the fast path, triggering the
    // short-circuit in `fastPathFilesToTypecheck`.
    sendAsync(*changeFile("bar.rb",
                          "# typed: true\n"
                          "class Bar\n"
                          "  extend T::Sig\n"
                          "  sig { returns(Integer) }\n"
                          "  def compute_value = 0\n"
                          "end\n",
                          4));

    // Unblock the slow path.
    setSlowPathBlocked(false);

    // Drain the pipeline with a fence.
    assertErrorDiagnostics(send(LSPMessage(make_unique<NotificationMessage>("2.0", LSPMethod::SorbetFence, 20))), {});
}

// This case shows how preemption works when the preemption tasks occur in a stratum that's at or before the one that
// contains the file that kicked off the slow path.
TEST_CASE_FIXTURE(MultithreadedProtocolTest, "PackageDirectedPreemptionBeforeSlowPathStratum") {
    auto opts = make_shared<realmain::options::Options>();
    opts->cacheSensitiveOptions.sorbetPackages = true;
    opts->packageDirected = true;
    resetState(opts);

    vector<pair<string, string>> files = {
        {"__package.rb", PackageTextBuilder().withName("Root").withExports({"Root::A"}).build()},
        {"a.rb", "# typed: true\n"
                 "module Root\n"
                 "  class A\n"
                 "    def fun\n"
                 "    end\n"
                 "  end\n"
                 "end\n"},
        {"foo/__package.rb",
         PackageTextBuilder().withName("Root::Foo").withExports({"Root::Foo::A"}).withImports({"Root"}).build()},
        {"foo/a.rb", "# typed: true\n"
                     "module Root::Foo\n"
                     "  class A\n"
                     "    def foo\n"
                     "      Root::A.new()\n"
                     "    end\n"
                     "  end\n"
                     "end\n"},
    };

    writeFilesToFS(files);

    for (auto &[path, _] : files) {
        this->lspWrapper->opts->inputFileNames.emplace_back(fmt::format("{}/{}", this->rootPath, path));
    }

    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    assertErrorDiagnostics(send(*openFile("a.rb", files[1].second)), {});

    assertErrorDiagnostics(send(*openFile("foo/b.rb", "# typed: true\n"
                                                      "module Root::Foo\n"
                                                      "  class Bar\n"
                                                      "  end\n"
                                                      "end\n")),
                           {});

    setSlowPathBlocked(true);

    {
        auto cancellationExpected = false;

        // We're expecting one preemption from each of the following sub-cases.
        auto preemptionsExpected = 1;

        sendAsync(*changeFile("foo/b.rb",
                              "# typed: true\n"
                              "module Root::Foo\n"
                              "  class Bar\n"
                              "    class Inner\n"
                              "      def bar\n"
                              "        Root::A.n\n"
                              "      end\n"
                              "    end\n"
                              "  end\n"
                              "end\n",
                              2, cancellationExpected, preemptionsExpected));
    }

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    SUBCASE("completion") {
        // Send a completion request at the end of `Root::A.`
        sendAsync(*completion("foo/b.rb", 5, 16));

        setSlowPathBlocked(false);

        // This is the completion result
        auto resp = readAsync();
        REQUIRE(resp->isResponse());
        CHECK_EQ(resp->asResponse().requestMethod, LSPMethod::TextDocumentCompletion);
    }

    SUBCASE("hover") {
        // Send a hover request at the end of `Root::A.`
        sendAsync(*hover("foo/b.rb", 5, 14));

        setSlowPathBlocked(false);

        // This is the hover result
        auto resp = readAsync();
        REQUIRE(resp->isResponse());
        CHECK_EQ(resp->asResponse().requestMethod, LSPMethod::TextDocumentHover);
    }

    SUBCASE("fast_path_edit") {
        // Make a fast-path edit to a.rb
        sendAsync(*changeFile("a.rb",
                              "# typed: true\n"
                              "module Root\n"
                              "  class A\n"
                              "    def fun\n"
                              "      A.new\n"
                              "    end\n"
                              "  end\n"
                              "end\n",
                              2));

        setSlowPathBlocked(false);

        // We should see a fast path start notification
        {
            auto diag = readAsync();
            REQUIRE(diag->isNotification());
            REQUIRE_EQ(diag->asNotification().method, LSPMethod::SorbetTypecheckRunInfo);
            auto &runInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(diag->asNotification().params);
            CHECK_EQ(runInfo->typecheckingPath, TypecheckingPath::Fast);
            CHECK_EQ(runInfo->status, SorbetTypecheckRunStatus::Started);
            CHECK(runInfo->filesTypechecked.empty());
        }

        // And then a fast path end notification
        {
            auto diag = readAsync();
            REQUIRE(diag->isNotification());
            REQUIRE_EQ(diag->asNotification().method, LSPMethod::SorbetTypecheckRunInfo);
            auto &runInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(diag->asNotification().params);
            CHECK_EQ(runInfo->typecheckingPath, TypecheckingPath::Fast);
            CHECK_EQ(runInfo->status, SorbetTypecheckRunStatus::Ended);
            REQUIRE_EQ(runInfo->filesTypechecked.size(), 1);
            CHECK_EQ(runInfo->filesTypechecked.front(), fmt::format("{}/a.rb", this->rootPath));
        }
    }

    // This is the slow path publishing a notification about the call to Roo::A.n
    auto diag = readAsync();
    REQUIRE(diag->isNotification());
    CHECK_EQ(diag->asNotification().method, LSPMethod::TextDocumentPublishDiagnostics);

    // Finally, we'll see the slow path end
    {
        auto diag = readAsync();
        REQUIRE(diag->isNotification());
        REQUIRE_EQ(diag->asNotification().method, LSPMethod::SorbetTypecheckRunInfo);
        auto &runInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(diag->asNotification().params);
        CHECK_EQ(runInfo->typecheckingPath, TypecheckingPath::Slow);
        CHECK_EQ(runInfo->status, SorbetTypecheckRunStatus::Ended);
    }
}

// This case shows how preemption works when the preemption tasks occur in a stratum that's after the one that contains
// the file that kicked off the slow path.
TEST_CASE_FIXTURE(MultithreadedProtocolTest, "PackageDirectedPreemptionAfterSlowPathStratum") {
    auto opts = make_shared<realmain::options::Options>();
    opts->cacheSensitiveOptions.sorbetPackages = true;
    opts->packageDirected = true;
    resetState(opts);

    vector<pair<string, string>> files = {
        {"__package.rb", PackageTextBuilder().withName("Root").withExports({"Root::A"}).build()},
        {"a.rb", "# typed: true\n"
                 "module Root\n"
                 "  class A\n"
                 "    def fun\n"
                 "    end\n"
                 "  end\n"
                 "end\n"},
        {"foo/__package.rb",
         PackageTextBuilder().withName("Root::Foo").withExports({"Root::Foo::A"}).withImports({"Root"}).build()},
        {"foo/a.rb", "# typed: true\n"
                     "module Root::Foo\n"
                     "  class A\n"
                     "    def foo\n"
                     "      Root::A.new()\n"
                     "    end\n"
                     "  end\n"
                     "end\n"},
    };

    writeFilesToFS(files);

    for (auto &[path, _] : files) {
        this->lspWrapper->opts->inputFileNames.emplace_back(fmt::format("{}/{}", this->rootPath, path));
    }

    auto initOptions = make_unique<SorbetInitializationOptions>();
    initOptions->enableTypecheckInfo = true;
    assertErrorDiagnostics(
        initializeLSP(true /* supportsMarkdown */, true /* supportsCodeActionResolve */, move(initOptions)), {});

    // Add a new file in the first stratum
    assertErrorDiagnostics(send(*openFile("b.rb", "# typed: true\n"
                                                  "module Root\n"
                                                  "  class B\n"
                                                  "  end\n"
                                                  "end\n")),
                           {});

    assertErrorDiagnostics(send(*openFile("foo/a.rb", files[3].second)), {});

    setSlowPathBlocked(true);

    {
        auto cancellationExpected = false;

        // We're expecting one preemption from each of the following sub-cases.
        auto preemptionsExpected = 1;

        // We're modifying a file in the first stratum, and making preemption queries for files in the second.
        sendAsync(*changeFile("b.rb",
                              "# typed: true\n"
                              "module Root\n"
                              "  class B\n"
                              "    class Inner\n"
                              "      def bar\n"
                              "        Root::A.n\n"
                              "      end\n"
                              "    end\n"
                              "  end\n"
                              "end\n",
                              2, cancellationExpected, preemptionsExpected));
    }

    // Wait for typechecking to start.
    {
        auto status = getTypecheckRunStatus(*readAsync());
        REQUIRE(status.has_value());
        REQUIRE_EQ(*status, SorbetTypecheckRunStatus::Started);
    }

    SUBCASE("hover") {
        // Send a hover request at the end of `Root::A.`
        sendAsync(*hover("foo/a.rb", 4, 12));

        setSlowPathBlocked(false);

        // This is the slow path publishing a notification about the call to Roo::A.n in b.rb, which happens on an
        // earlier stratum than the hover in foo/a.rb.
        auto diag = readAsync();
        REQUIRE(diag->isNotification());
        CHECK_EQ(diag->asNotification().method, LSPMethod::TextDocumentPublishDiagnostics);

        // This is the hover result
        auto resp = readAsync();
        REQUIRE(resp->isResponse());
        CHECK_EQ(resp->asResponse().requestMethod, LSPMethod::TextDocumentHover);
    }

    SUBCASE("fast_path_edit") {
        // Make a fast-path edit to foo/a.rb
        sendAsync(*changeFile("foo/a.rb",
                              "# typed: true\n"
                              "module Root::Foo\n"
                              "  class A\n"
                              "    def foo\n"
                              "      Root::A.new()\n"
                              "      puts :hi\n"
                              "    end\n"
                              "  end\n"
                              "end\n",
                              2));

        setSlowPathBlocked(false);

        // This is the slow path publishing a notification about the call to Roo::A.n in b.rb, which happens on an
        // earlier stratum than the fast path edit to foo/a.rb
        auto diag = readAsync();
        REQUIRE(diag->isNotification());
        CHECK_EQ(diag->asNotification().method, LSPMethod::TextDocumentPublishDiagnostics);

        // We should see a fast path start notification
        {
            auto diag = readAsync();
            REQUIRE(diag->isNotification());
            REQUIRE_EQ(diag->asNotification().method, LSPMethod::SorbetTypecheckRunInfo);
            auto &runInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(diag->asNotification().params);
            CHECK_EQ(runInfo->typecheckingPath, TypecheckingPath::Fast);
            CHECK_EQ(runInfo->status, SorbetTypecheckRunStatus::Started);
            CHECK(runInfo->filesTypechecked.empty());
        }

        // And then a fast path end notification
        {
            auto diag = readAsync();
            REQUIRE(diag->isNotification());
            REQUIRE_EQ(diag->asNotification().method, LSPMethod::SorbetTypecheckRunInfo);
            auto &runInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(diag->asNotification().params);
            CHECK_EQ(runInfo->typecheckingPath, TypecheckingPath::Fast);
            CHECK_EQ(runInfo->status, SorbetTypecheckRunStatus::Ended);
            REQUIRE_EQ(runInfo->filesTypechecked.size(), 1);
            CHECK_EQ(runInfo->filesTypechecked.front(), fmt::format("{}/foo/a.rb", this->rootPath));
        }
    }

    // Finally, we'll see the slow path end
    {
        auto diag = readAsync();
        REQUIRE(diag->isNotification());
        REQUIRE_EQ(diag->asNotification().method, LSPMethod::SorbetTypecheckRunInfo);
        auto &runInfo = get<unique_ptr<SorbetTypecheckRunInfo>>(diag->asNotification().params);
        CHECK_EQ(runInfo->typecheckingPath, TypecheckingPath::Slow);
        CHECK_EQ(runInfo->status, SorbetTypecheckRunStatus::Ended);
    }
}

} // namespace sorbet::test::lsp
