Bun


В последнее время мне очень нравится Bun. Это новая среда исполнения JavaScript / TypeScript, схожая с Deno / Node. Она имеет одно преимущество по сравнению с другими средами исполнения, которое очень важно для меня: очень быстрый запуск (по крайней мере, для JS). Когда я впервые запустил в ней небольшой кусок кода, то просто не мог поверить.

Когда я перешёл с Ruby на Node, меня оттолкнуло то, что тесты в Node выполняются о-о-очень медленно. Написание одной и той же бизнес-логики и её тестирование на этих языках — совершенно разный опыт. Неудивительно, что сообщество JS-разработчиков ненавидит юнит-тестирование, когда нужно думать, например, распределять ли тесты на несколько файлов, или нет.

Однако на то есть причина. Как бы вы ни оптимизировали инструменты для выполнения тестов наподобие Vitest, Jest или Ava, первый прогон теста (без watch) всегда будет выполняться чрезвычайно медленно в Node, потому что для запуска V8 и разрешения модулей требуется куча времени. Когда ты распределяешь работу на несколько процессов, чтобы использовать все ядра, это требует ещё больше ресурсов!

Bun выполняет 266 тестов с SSR для react-dom на 40% быстрее, чем jest просто выводит номер своей версии, Джаред Саммер

То есть если вам нужен быстрый набор тестов, вы вынуждены уменьшить время запуска VM и ускорить разрешение модулей. Кроме того, можно повысить оптимизацию, реализовав инструмент выполнения на языке, гораздо более быстром, чем JS, и именно это делает Bun.

Кейс


Я решил проверить Bun и создал приближённый к реальному проект-бенчмарк для этого фреймворка тестирования. Насколько быстро выполнится набор тестов с 400 сквозными изолированными http-тестами с доступом к базе данных?

Написав три строки кода и запустив набор, я столкнулся с проблемой. Я попытался передать 0 в качестве параметра Bun.serve, чтобы динамически назначать порт для созданного http-сервера, но… быстро осознал, почему Bun всё ещё находится в состоянии бета-версии.

// benchmark/request.spec.ts
test('e2e bun serve test', () => {
  const server = Bun.serve({ port: 0 });
  // ...запрос к API и так далее
  server.stop();
})

$ bun wiptest
error: Uncaught (in promise)
  TypeError: Invalid port: must be > 0

Слой совместимости с Node оказался не совсем… совместимым. Но я решил не отказываться от идеи, а заставить его работать!

Рецепт


Ниже я расскажу о процессе, который я выполнил, чтобы стать контрибьютором Bun, и как приступил к работе над проектом, который я не знаю. Надеюсь, это поможет вам набраться уверенности, чтобы браться за новые задачи.

▍ 0. Спросить


Если у вас есть возможность спросить предыдущих контрибьюторов или мейнтейнеров о фиче/проблеме, которую вы пытаетесь решить, то сделайте это. Всегда лучше спросить, чем тратить время на то, что не нужно или невозможно.

Я просто зашёл в issues GitHub и поискал по запросу "port 0" и-и-и… нашёл. Моя проблема уже какое-то время назад была задокументирована, но пока за неё никто не брался. Мейнтейнеры не пометили её как ненужную, невозможную и так далее. У них просто не было времени реализовать её. Поэтому я решил попробовать.

▍ 1. Создать спецификацию фичи/задачи


Крайне важно попытаться максимально сузить рамки задачи. Многие из нас были в ситуации, когда от нас просили реализовать что-то без каких-то спецификаций или объяснений. Сложно работать над тем, что ты не можешь сформулировать.

В моём случае задача была довольно проста: «Bun не позволяет передавать 0 в качестве значения прослушиваемого порта. Он выбрасывает ошибку, когда не должен этого делать». Создав определение задачи, я мог начать работать над ней.

▍ 2. Выполнить тесты


Следующий этап всегда одинаков, и он критически важен. Документация устаревает, тикеты/issue теряются. В кодовой базе нет ничего столь же надёжного, как тесты. Если вы хотите знать, как/почему что-то работает, и работает ли, выполните тесты. Если у приложения нет тестов, вы обязаны их написать. Именно поэтому контрибьютирнг в OSS с надёжным набором тестов гораздо проще, чем в проекты без него. Представьте, что вы сможете асинхронно, без необходимости ожидания анализа проверить, работает ли написанный/модифицированный вами код.

Я настроил среду разработки согласно инструкции в репозитории Bun. Потом запустил набор тестов. Всё зелёное!

Если все тесты успешно проходят, можно продолжать работу. Если некоторые тесты не проходят, проверьте, возможно они «всегда красные», потому что в некоторых кодовых базах есть хорошо известные капризные тесты.

# выполняет все тесты в репозитории bun
$ bun run test
567 tests passed, 0 tests failed

▍ 3. Написать тест


В пункте 1 я создал спецификацию нашей задачи. Теперь я могу написать автоматизированную spec. Для этого я обычно беру другую spec, которая уже существует, удаляю всё, что не является бойлерплейтом и добавляю логику, которую хочу протестировать.

// bun/test/bun.js/bun-server.spec.ts
import { expect, test } from "bun:test";

test("Server initializes with 0 port", () => {
  const server = Bun.serve({
    fetch: () => new Response("Hello"),
    port: 0,
  });

  expect(server).toBeDefined();
  server.stop();
});

$ bun run test
567 tests passed, 1 test failed
  bun/test/bun.js/bun-server.spec.ts
  ✖ Server initializes with 0 port
  Error: Uncaught (in promise)
    TypeError: Invalid port: must be > 0

Новая спецификация обязана не соответствовать. Если она изначально успешно проходит тест, то значит, вы делаете что-то не так, или баг, который вы пытаетесь устранить, не существует.

Теперь вся наша работа будет заключаться в том, чтобы тест был пройден успешно.

▍ 4. Найти и изменить код


Где искать? Просто выполните полнотекстовый поиск выдаваемой ошибки или пройдитесь по трассировке стека.

Я поискал выдаваемое сообщение об ошибке, и вуаля — нашёл его, код, выкидывающий ошибку.

// bun/src/bun.js/api/server.zig
if (args.port == 0) {
    JSC.throwInvalidArguments(
      "Invalid port: must be > 0", .{}, global, exception
    );
}

Почему он здесь есть? Не осталось никакой документации + отсутствуют issue в GH. Поэтому я просто удалил его и-и-и… тесты начали выполняться успешно! Потрясающе. Это было просто, слишком просто…

Если задуматься на секунду, то можно понять, что если удалил строку кода и все тесты начали успешно выполняться, в том числе и новый, то это значит, что данный код раньше не тестировался.

▍ 3, часть 2. Написать другой тест — ломаем решение


Если он не тестировался, то наша обязанность при внесении изменений в него протестировать его. Чтобы в будущем, когда кто-нибудь захочет его изменить, это можно было бы сделать без неизвестных последствий. Как же это сделать?

Нужно поломать решение! Написать другой тест, чтобы он попытался отправить http-запрос этому серверу с динамическим портом, и проверить, отвечает ли он правильно. И-и-и… он не отвечает.

// bun/test/bun.js/bun-server.spec.ts
// ...наш предыдущий тест

test("Server allows connecting to server", async () => {
  const server = Bun.serve({
    fetch: () => new Response("Hello"),
    port: 0,
  });

  const response = await fetch(
    `http://localhost:${server.port}`
  );
  expect(await response.text()).toBe("Hello");
  server.stop();
});

$ bun run test
568 tests passed, 1 test failed
  bun/test/bun.js/bun-server.spec.ts
  ✖ Server allows connecting to server
    Error: Unable to connect to server at http://localhost:0

Ошибка отличается. При получении порта он возвращает 0, а не порт, назначенный операционной системой.

Наверно, вы догадались, что нужно сделать дальше — заставить тест завершаться успешно.

▍ 4, часть 2. Возвращаемся к изменению кода


Мы не получаем порт динамически от инстанса сервера, он просто тот же, что я передал.

// bun/src/bun.js/api/server.zig
pub fn getPort(this: *ThisServer) JSC.JSValue {
    return JSC.JSValue.jsNumber(this.config.port);
}

Мне нужно внести изменения, чтобы динамически получать от ОС порт.

Это определённо более сложная задача, нужно писать код на совершенно неизвестном мне языке Zig. Поэтому я снова разбил задачу на части. Вместо того, чтобы пытаться решить задачу отправки реального порта, я решил отправлять что угодно из Zig в наш мир JS.
Столкнувшись с мелкой проблемой, для которой у вас уже есть автоматизированный тест, вы решите её, это лишь вопрос времени. Тест нужен, чтобы сократить цикл обратной связи. Вам понадобится секунда, чтобы проверить, делаете ли вы всё правильно.

Сначала я верну статический порт, однако отличающийся от 0, и проверю, проваливается ли мой тест так, как я хочу, чтобы он проваливался (это тоже обратная связь). Если сервер не работает, я возвращаю порт конфигурации, в противном случае — мою имитацию порта.

// bun/src/bun.js/api/server.zig
pub fn getPort(this: *ThisServer) JSC.JSValue {
    if (this.config.port == 0) {
        return JSC.JSValue.jsNumber(1234);
    }
    return JSC.JSValue.jsNumber(this.config.port);
}

$ bun run test
568 tests passed, 1 test failed
  bun/test/bun.js/bun-server.spec.ts
  ✖ Server allows connecting to server
    Error: Unable to connect to server at http://localhost:1234

И он успешно провалился! Это замечательно, поскольку показывает, что я меняю то, что нужно. Единственное, что осталось сделать — узнать, как получить порт от процесса.

Я просмотрел код в поисках мест, где сокет раскрывал любые значения. Оказалось, что это происходит в нескольких местах, например, при закрытии сокета. Несколькими копипастами позже, избавившись от ошибок компилятора и изменив моё решение с имитацией на новое, я решил задачу. Тесты теперь завершались успешно.

// bun/src/bun.js/api/server.zig
pub fn getPort(this: *ThisServer) JSC.JSValue {
  return JSC.JSValue.jsNumber(this.config.port);
  var listener = this.listener orelse return JSC.JSValue.jsNumber(this.config.port);
  return JSC.JSValue.jsNumber(listener.getLocalPort());
}

// bun/src/deps/uws.zig
pub inline fn getLocalPort(this: *ThisApp.ListenSocket) i32 {
  if (comptime is_bindgen) {
    unreachable;
  }
  return us_socket_local_port(ssl_flag, @ptrCast(*uws.Socket, this));
}

$ bun run test
569 tests passed, 0 tests failed

PR, CR, merge, релизим. Готово.


Подведём итог


Чтобы стать контрибьютором любого проекта, вам необязательно знать его досконально. Более того, вы никогда не будете знать всего о проекте, если только не напишете каждую строку его кода. Но даже в этом случае часть вы забудете.

Нужно быть уверенным в работе людей, создающих проект, и в наборе тестов. Если такая уверенность есть, то вам даже не нужно знать, делаете ли вы всё правильно. Тесты будут проваливаться или CR будет отклонять ваш код. И это нормально.

Процесс прост:

  1. Выполняете тесты, убеждаетесь, что все они зелёные.
  2. Пишете тест, который не проходит.
  3. Меняете код.
  4. Думаете, нужно ли добавить ещё один тест.
  5. Если обнаружили тест, который нужно написать, возвращаетесь к пункту 1. Если не можете придумать тест, то работа закончена.

Если вы мейнтейнер, то отдайте высокий приоритет созданию простого и надёжного набора тестов. В противном случае вы обречены на написание чрезмерно сложной документации, объясняющей, как написать строку кода в вашем проекте, не сломав его. В проекте с набором тестов входной барьер гораздо ниже. И именно поэтому я всегда пытаюсь убедить людей писать тесты, особенно в OSS. Дело не только в качестве кода, но и в сообществе, в простоте участия в проекте.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх ????️

Комментарии (0)