> For the complete documentation index, see [llms.txt](https://eda-1.gitbook.io/lgwt/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://eda-1.gitbook.io/lgwt/sozdanie-prilozheniya/revisiting-time-with-synctest.md).

# Пересмотр работы со временем с помощью testing/synctest

[**Весь код для этой главы вы найдете здесь**](https://github.com/quii/learn-go-with-tests/tree/main/synctest)

В [главе о времени](/lgwt/sozdanie-prilozheniya/time.md) мы предоставили нашему CLI для покера возможность планировать уведомления «блайнд теперь повышается», используя `time.AfterFunc`. Тестирование этого было непростым: `time.AfterFunc` запускает свою функцию обратного вызова в собственной горутине после истечения реального промежутка времени, а в Go нельзя сравнивать функции, поэтому мы не могли легко проверить, что было запланировано.

Мы решили эту проблему с помощью знакомого инструмента: внедрения зависимостей. Мы определили интерфейс `BlindAlerter`, а в наших тестах мы заменили реальную реализацию на подделку, которая просто записывает, что её попросили запланировать, примерно так:

```go
type BlindAlerter interface {
	ScheduleAlertAt(duration time.Duration, amount int)
}

type SpyBlindAlerter struct {
	Alerts []struct {
		At     time.Duration
		Amount int
	}
}

func (s *SpyBlindAlerter) ScheduleAlertAt(at time.Duration, amount int) {
	s.Alerts = append(s.Alerts, struct {
		At     time.Duration
		Amount int
	}{at, amount})
}
```

Это хорошая архитектура, и тесты, которые она позволяет создавать, быстрые и надёжные. Но присмотритесь внимательнее к тому, что они на самом деле покрывают. Они утверждают такие вещи, как «`ScheduleAlertAt` был вызван с `10 * time.Minute` и `200`». Они никогда не позволяют запускаться *настоящему* оповещателю. Единственная часть кода, которая фактически вызывает `time.AfterFunc` и что-то печатает, часть с реальным потенциалом ошибки, никогда не проверяется тестом.

Это не недосмотр, это компромисс. Правильное тестирование реального оповещателя на основе `time.AfterFunc` означало бы либо ожидание реальных минут для завершения теста, либо сокращение продолжительности до миллисекунд и надежду, что машина, на которой выполняется тест, не слишком занята, чтобы справиться. Ни то, ни другое не привлекательно. Поэтому исторически мы просто этого не делали.

Начиная с Go 1.25, появился третий вариант: [`testing/synctest`](https://pkg.go.dev/testing/synctest). Он позволяет нам запускать реальный, неизменённый код, который использует `time.Sleep`, `time.AfterFunc` и подобные функции, внутри теста, который имеет полный контроль над временем: без ожидания, без нестабильности, без сокращения продолжительности и надежды на лучшее.

В этой главе мы с нуля создадим настоящий оповещатель и протестируем его с помощью `synctest`. Но давайте сначала заслужим это, увидев, от чего именно он нас избавляет.

## Краткая информация о `testing/synctest`

`testing/synctest` запускает функцию внутри изолированного **пузыря**. Внутри этого пузыря:

* Пакет `time` использует фальшивые часы. Они стартуют в полночь UTC 1 января 2000 года и движутся только вперёд.
* Фальшивое время продвигается только тогда, когда каждая горутина в пузыре **долгосрочно заблокирована**: заблокирована таким образом, что только другая горутина в том же пузыре может её разблокировать. `time.Sleep`, блокирующий приём из канала, созданного внутри пузыря, `sync.Cond.Wait` и `sync.WaitGroup.Wait` — всё это считается. Блокировка `sync.Mutex` не считается, потому что мьютексы обычно удерживаются лишь на короткое время.
* `synctest.Test(t, f)` запускает `f` в новом пузыре и не возвращается до тех пор, пока каждая горутина, созданная внутри него, не завершит работу. Если пузырь оказывается долгосрочно заблокированным без возможности продвижения, тест завершается с ошибкой как тупик, вместо того чтобы зависать навсегда.
* `synctest.Wait()` блокирует вызывающую горутину до тех пор, пока каждая *другая* горутина в пузыре не будет долгосрочно заблокирована, затем возвращается. Именно так вы даёте фоновой работе завершиться, прежде чем делать утверждение.

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

## Сначала напишите тест

Начнём с очевидного, прежде чем вообще использовать `synctest`: `BlindAlerter`, который, получив продолжительность и сумму, ждёт эту продолжительность, а затем записывает сообщение куда-либо, протестированный в реальном времени.

```go
package poker

import (
	"bytes"
	"testing"
	"time"
)

func TestStdOutAlerter(t *testing.T) {
	out := &bytes.Buffer{}
	alerter := StdOutAlerter(out)

	alerter.ScheduleAlertAt(5*time.Second, 100)

	time.Sleep(6 * time.Second)

	want := "Blind is now 100"
	if out.String() != want {
		t.Errorf("got %q, want %q", out.String(), want)
	}
}
```

Мы ждём немного дольше запланированного уведомления (6 секунд, а не 5), чтобы дать горутине, которую запускает `time.AfterFunc`, момент для фактического выполнения, прежде чем мы проверим.

## Попробуйте запустить тест

Мы ещё не написали никакого рабочего кода, поэтому он не скомпилируется:

```
./blind_alerter_test.go:11:13: undefined: StdOutAlerter
```

## Напишите достаточно кода, чтобы тест прошел

```go
package poker

import (
	"fmt"
	"io"
	"time"
)

// BlindAlerter schedules alerts for blind amounts.
type BlindAlerter interface {
	ScheduleAlertAt(duration time.Duration, amount int)
}

// BlindAlerterFunc allows you to implement BlindAlerter with a function.
type BlindAlerterFunc func(duration time.Duration, amount int)

// ScheduleAlertAt is BlindAlerterFunc's implementation of BlindAlerter.
func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) {
	a(duration, amount)
}

// StdOutAlerter returns a BlindAlerterFunc that schedules alerts and prints them to out.
func StdOutAlerter(out io.Writer) BlindAlerterFunc {
	return func(duration time.Duration, amount int) {
		time.AfterFunc(duration, func() {
			fmt.Fprintf(out, "Blind is now %d", amount)
		})
	}
}
```

Запустите его:

```
=== RUN   TestStdOutAlerter
--- PASS: TestStdOutAlerter (6.00s)
PASS
ok  	github.com/quii/learn-go-with-tests/synctest/v1	6.191s
```

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

## Представляем synctest

Давайте обернём тот же тест в пузырь и посмотрим, что произойдёт, если мы слишком оптимистично относимся к тому, что делают для нас «фальшивые часы»:

```go
func TestStdOutAlerter(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		out := &bytes.Buffer{}
		alerter := StdOutAlerter(out)

		alerter.ScheduleAlertAt(5*time.Second, 100)

		want := "Blind is now 100"
		if out.String() != want {
			t.Errorf("got %q, want %q", out.String(), want)
		}
	})
}
```

Мы полностью убрали задержку: наверняка фальшивые часы справятся с этим теперь сами? Но нет:

```
=== RUN   TestStdOutAlerter
    blind_alerter_test.go:19: got "", want "Blind is now 100"
--- FAIL: TestStdOutAlerter (0.00s)
FAIL
```

Фальшивые часы пузыря не движутся вперёд по собственному таймеру. Они продвигаются только тогда, когда что-то в пузыре долгосрочно блокируется, ожидая этого. Нам всё ещё нужно указать, чего мы ждём; мы просто больше не должны платить за это реальными секундами. Давайте вернём задержку:

```go
func TestStdOutAlerter(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		out := &bytes.Buffer{}
		alerter := StdOutAlerter(out)

		alerter.ScheduleAlertAt(5*time.Second, 100)

		time.Sleep(6 * time.Second)

		want := "Blind is now 100"
		if out.String() != want {
			t.Errorf("got %q, want %q", out.String(), want)
		}
	})
}
```

```
=== RUN   TestStdOutAlerter
--- PASS: TestStdOutAlerter (0.00s)
PASS
ok  	github.com/quii/learn-go-with-tests/synctest/v2	0.133s
```

Проходит, и мгновенно: `time.Sleep(6 * time.Second)` внутри пузыря ничего не стоит в реальном времени. Выглядит завершённым. Давайте просто перепроверим, как он выдерживает `-race`, по привычке:

```
go test -race ./...
```

```
==================
WARNING: DATA RACE
Read at 0x00c00010e630 by goroutine 9:
  bytes.(*Buffer).String()
      /usr/local/go/src/bytes/buffer.go:77 +0x174
  github.com/quii/learn-go-with-tests/synctest/v2.TestStdOutAlerter.func1()
      blind_alerter_test.go:20 +0x15c
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1934 +0x164

Previous write at 0x00c00010e630 by goroutine 11:
  bytes.(*Buffer).grow()
      /usr/local/go/src/bytes/buffer.go:143 +0x354
  bytes.(*Buffer).Write()
      /usr/local/go/src/bytes/buffer.go:185 +0xb4
  fmt.Fprintf()
      /usr/local/go/src/fmt/print.go:225 +0x94
  github.com/quii/learn-go-with-tests/synctest/v2.TestStdOutAlerter.func1.BlindAlerterFunc.ScheduleAlertAt.TestStdOutAlerter.func1.StdOutAlerter.1.2()
      blind_alerter.go:26 +0x6c

Goroutine 11 (finished) created at:
  time.goFunc()
      /usr/local/go/src/time/sleep.go:215 +0x40
==================
--- FAIL: TestStdOutAlerter (0.00s)
    testing.go:1617: race detected during execution of test
FAIL
```

Ой. Обратите внимание на «Goroutine 11 (finished)»: запись действительно произошла до нашего чтения, каждый раз, когда мы его запускали. Функционально, тест на самом деле никогда не может провалиться по утверждению. Но детектор гонки данных не спрашивает «что-то пошло не так на этот раз?»; он спрашивает «есть ли что-то, что *гарантирует*, что это не может произойти?». `time.Sleep` в нашей горутине и функция обратного вызова `time.AfterFunc` в её собственной горутине — это всего лишь две независимо запланированные горутины. Ничто в «я немного поспал» не обещает, что другая горутина закончила работу с общей памятью, независимо от того, насколько щедра задержка. Увеличение задержки не исправляет это, независимо от того, насколько она велика; это просто приводит к тому, что ошибка проявляется реже за пределами `-race`.

Стоит подумать об этом секунду: эта же самая гонка данных уже скрывалась в нашей самой первой, простой версии, работающей в реальном времени, также с настоящим `time.Sleep`. Мы просто никогда не думали проверять: кто запускает `-race` повторно на тесте, который уже занимает шесть реальных секунд для выполнения?

## Сначала напишите тест

Что нам нужно, так это реальная точка синхронизации: что-то, что не просто делает запись *вероятно* первой, но фактически устанавливает, что это произошло. Именно для этого и нужен `synctest.Wait()`:

```go
func TestStdOutAlerter(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		out := &bytes.Buffer{}
		alerter := StdOutAlerter(out)

		alerter.ScheduleAlertAt(5*time.Second, 100)

		time.Sleep(6 * time.Second)
		synctest.Wait()

		want := "Blind is now 100"
		if out.String() != want {
			t.Errorf("got %q, want %q", out.String(), want)
		}
	})
}
```

## Рефакторинг

В рабочем коде ничего менять не нужно, но давайте убедимся, что он действительно выдерживает, многократно, при `-race`:

```
ok  	github.com/quii/learn-go-with-tests/synctest/v3	1.153s
ok  	github.com/quii/learn-go-with-tests/synctest/v3	1.143s
ok  	github.com/quii/learn-go-with-tests/synctest/v3	1.148s
ok  	github.com/quii/learn-go-with-tests/synctest/v3	1.147s
ok  	github.com/quii/learn-go-with-tests/synctest/v3	1.143s
```

Чисто, каждый раз. Очень хорошо.

## Сначала напишите тест

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

```go
func TestStdOutAlerter(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		out := &bytes.Buffer{}
		alerter := StdOutAlerter(out)

		alerter.ScheduleAlertAt(5*time.Second, 100)

		synctest.Wait()
		if out.String() != "" {
			t.Errorf("did not expect anything to be printed yet, got %q", out.String())
		}

		time.Sleep(5 * time.Second)
		synctest.Wait()

		want := "Blind is now 100"
		if out.String() != want {
			t.Errorf("got %q, want %q", out.String(), want)
		}
	})
}
```

Ничего другого в пузыре не происходит в тот момент, когда мы планируем уведомление, поэтому `Wait()` должен вернуться сразу: пока нечего ждать, так что `out` должен быть всё ещё пустым. Запустите его:

```
=== RUN   TestStdOutAlerter
--- PASS: TestStdOutAlerter (0.00s)
PASS
```

Проходит. Давайте проверим `-race`, как мы теперь знаем, что должны:

```
==================
WARNING: DATA RACE
Write at 0x00c00011c630 by goroutine 11:
  bytes.(*Buffer).grow()
      /usr/local/go/src/bytes/buffer.go:143 +0x354
  bytes.(*Buffer).Write()
      /usr/local/go/src/bytes/buffer.go:185 +0xb4
  fmt.Fprintf()
      /usr/local/go/src/fmt/print.go:225 +0x94
  github.com/quii/learn-go-with-tests/synctest/v4.TestStdOutAlerter.func1.BlindAlerterFunc.ScheduleAlertAt.TestStdOutAlerter.func1.StdOutAlerter.1.2()
      blind_alerter.go:26 +0x6c

Previous read at 0x00c00011c630 by goroutine 9:
  bytes.(*Buffer).String()
      /usr/local/go/src/bytes/buffer.go:77 +0x164
  github.com/quii/learn-go-with-tests/synctest/v4.TestStdOutAlerter.func1()
      blind_alerter_test.go:18 +0x14c
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1934 +0x164

Goroutine 11 (running) created at:
  time.goFunc()
      /usr/local/go/src/time/sleep.go:215 +0x40
==================
--- FAIL: TestStdOutAlerter (0.00s)
    testing.go:1617: race detected during execution of test
FAIL
```

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

Проблема в том, что один только `Wait()` ничем не ограничивает, как далеко он готов позволить фальшивым часам продвигаться. На тот момент в пузыре нет других горутин, и нет предстоящего дедлайна, которого *нам* нужно ждать. Остался только 5-секундный таймер оповещения, находящийся в куче таймеров среды выполнения. Поэтому среда выполнения делает единственное полезное, что может: она запускает таймер для продвижения, создавая горутину 11 для выполнения нашей функции обратного вызова, *в то время как* `Wait()` всё ещё решает, возвращаться или нет. Горутина 11 действительно работает конкурентно с нашей проверкой, а не до неё. Иногда эта запись завершается первой; иногда нет. `bytes.Buffer`, к которому мы оба обращаемся, не имеет блокировки, поэтому ничто не делает это безопасным в любом случае.

Мы могли бы исправить это так же, как исправили бы любую гонку данных: обернуть `out` в `sync.Mutex` или использовать `atomic.Pointer[T]` из `sync/atomic` для более легкого решения. Но посмотрите, что на самом деле просят сделать `StdOutAlerter`: определить сообщение *и* выполнить побочный эффект его печати. Ничто в планировании оповещения о блайндах в покере не требует знания об `io.Writer`; это дело вызывающего кода. Это форма напряженности, к которой эта книга постоянно возвращается: [если ваши тесты доставляют вам боль, прислушайтесь к этому сигналу и подумайте о дизайне вашего кода](/lgwt/voprosy-i-otvety/http-handlers-revisited.md). Давайте сделаем так, чтобы оповещатель просто генерировал сообщение, когда придёт время, а вызывающий код решал, что с ним делать.

## Рефакторинг

Пересечение границы горутины — это именно то, для чего предназначены каналы. Как гласит пословица Go: [не общайтесь через общую память; делитесь памятью через общение](https://go.dev/blog/codelab-share). Вместо записи в общий `out`, наш оповещатель может отправить готовое сообщение по каналу:

```go
func NewAlerter() (BlindAlerterFunc, <-chan string) {
	alerts := make(chan string)

	scheduleAlertAt := func(duration time.Duration, amount int) {
		time.AfterFunc(duration, func() {
			alerts <- fmt.Sprintf("Blind is now %d", amount)
		})
	}

	return scheduleAlertAt, alerts
}
```

`BlindAlerter` и `BlindAlerterFunc` остаются точно такими, как были. Только `StdOutAlerter` (который, что показательно, больше не имел отношения к стандартному выводу) исчез, заменён на `NewAlerter`, возвращающий как оповещатель, так и канал для получения сообщений. Любой, кто хочет, чтобы эти оповещения были напечатаны (например, `main`), может итерировать по этому каналу и печатать их; это больше не проблема этого пакета.

Тест тоже становится проще:

```go
func TestNewAlerter(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		alerter, alerts := NewAlerter()

		alerter.ScheduleAlertAt(5*time.Second, 100)

		select {
		case got := <-alerts:
			t.Fatalf("did not expect an alert yet, got %q", got)
		default:
		}

		got := <-alerts
		want := "Blind is now 100"
		if got != want {
			t.Errorf("got %q, want %q", got, want)
		}
	})
}
```

Обратите внимание, что исчезло: нет `synctest.Wait()` нигде, нет пользовательского типа для защиты общего значения, вообще нет `time.Sleep`. Нам больше не нужен ни один из них.

`select` со случаем `default`, прямо из [главы о select](/lgwt/osnovy-go/select.md), никогда не блокируется: он либо принимает готовый случай, либо сразу переходит к `default`. В этот момент виртуальное время всё ещё находится на нуле, за пять полных (фальшивых) секунд до оповещения, поэтому синхронизировать нечего: конечно, ещё ничего не пришло. И `got := <-alerts` тоже не нуждается в подталкивании: это обычный блокирующий приём, поэтому пузырь делает именно то, для чего он предназначен: поскольку единственное, чего кто-либо в пузыре ждёт, это таймер, фальшивое время перескакивает прямо к моменту его срабатывания, будит горутину `time.AfterFunc` и разблокирует наш приём.

Запустите это с `-race`, многократно. Он остаётся зелёным: больше нет общей памяти, за которую можно конкурировать.

Сравните это с подходом `SpyBlindAlerter` из предыдущей главы. Это всё ещё отличный инструмент для другой задачи: он проверяет *что* планируется (для данного количества игроков, правильные ли суммы запланированы с правильными смещениями?), не заботясь вообще о реальном времени (полезно при тестировании арифметики, а не времени). `synctest` не заменил эту потребность; он заполняет пробел в покрытии, который всегда оставляет чистая подделка «что меня попросили сделать»: действительно ли сам механизм планирования работает?

## Завершение

### Что мы рассмотрели

* `synctest.Test` и идея пузыря с изолированными фальшивыми часами, который движется вперёд только тогда, когда что-то долгосрочно блокируется, а не сам по себе.
* «Долгосрочно заблокировано»: условие, которое позволяет фальшивому времени продвигаться, и почему `sync.Mutex` намеренно не считается (но приём из канала считается).
* Задержка «достаточно долго» — это не то же самое, что синхронизация: даже щедрая, всегда корректная на практике задержка всё равно является реальной гонкой данных, если ничто не обеспечивает упорядоченность.
* `synctest.Wait()` исправляет это для одной проверки, но вызов его без чего-либо ещё в пузыре, что его ограничивает, может позволить фальшивым часам продвинуться дальше, чем вы предполагали, что именно и произошло при тестировании случая «ещё ничего не произошло».
* Тест, выявляющий проблему в архитектуре, а не просто ошибку, и исправляющий архитектуру вместо использования блокировки.

### Подводные камни, на которые стоит обратить внимание

* Если горутина в пузыре всё ещё долгосрочно заблокирована, когда возвращается корневая функция пузыря, `synctest.Test` завершает тест с ошибкой как тупик, вместо того чтобы зависать; убедитесь, что фоновые горутины действительно завершаются.
* Каналы, таймеры и тикеры привязаны к пузырю, в котором они были созданы; использование их извне пузыря вызывает панику.
* Реальный сетевой и файловый ввод/вывод не является долгосрочно блокирующим, поэтому вы не можете управлять ими напрямую через фальшивые часы `synctest`; используйте что-то вроде `net.Pipe`, если вам нужна замена в памяти.
* Функция обратного вызова `time.AfterFunc` запускается в собственной горутине, без каких-либо гарантий синхронизации, которые даёт канал. Если ей приходится напрямую работать с общим состоянием, это состояние нуждается в собственной блокировке, как и любой другой конкурентный код.

### Дополнительные материалы

* [Блог Go: Тестирование конкурентного кода с testing/synctest](https://go.dev/blog/synctest)
* [Документация по пакету `testing/synctest`](https://pkg.go.dev/testing/synctest)
* [Заметки о выпуске Go 1.25](https://go.dev/doc/go1.25)

### Примечание о написании этой главы

Это первая глава в книге, написанная с помощью ИИ (Claude). Я хочу быть откровенным по этому поводу и о том, что на самом деле означала здесь «помощь», потому что это не было «опиши главу, получи главу».

Процесс во многом напоминал цикл TDD, которому эта книга учила на протяжении всего повествования: исследование реальной документации и исходного кода `testing/synctest` вместо догадок, создание небольших одноразовых программ для проверки утверждений, прежде чем написать единое слово прозы, и рассмотрение каждого утверждения из документации как того, что нужно проверять реальным запуском `go test -race`, а не доверять безоговорочно. Несколько подводных камней в этой главе, в первую очередь взаимодействие `Wait()` и детектора гонки данных, появились здесь только потому, что тест действительно провалился неожиданным образом, после нескольких раундов фактического запуска, и причину пришлось выяснять, прежде чем решить, что писать.

Дизайн сам по себе тоже изменил форму в процессе. Первый черновик предполагал, что `StdOutAlerter` будет писать прямо в `io.Writer`, что именно и является тем, против чего эта книга всегда выступала, когда тест начинает доставлять боль: [если ваши тесты доставляют вам боль, прислушайтесь к этому сигналу и подумайте о дизайне вашего кода](/lgwt/voprosy-i-otvety/http-handlers-revisited.md). Так мы и поступили, и в итоге получили версию на основе каналов, представленную выше, и более короткую, лучшую главу благодаря этому.

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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://eda-1.gitbook.io/lgwt/sozdanie-prilozheniya/revisiting-time-with-synctest.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
