# Endless 用户指南

Endless 是一个基于 [Ginkgo][ginkgo] 实现的 TiDB 端到端测试框架。它具备易于上手、
接口语义化、文档完善、与 [test-infra sdk][test-infra-sdk] 集成良好等重要特性。
目标是提高测试用例编写的效率。

Endless 适用于非 [mysql-test][mysql-test] 的功能测试和系统测试用例，这些用例通常不能对外开源（出于商业考虑）。

Endless 基于 [test-infra](https://github.com/pingcap/test-infra) 完成资源管理工作，并基于
[Gomega](https://onsi.github.io/gomega/) 和 [Ginkgo](https://onsi.github.io/ginkgo/) 提供了
case 管理和一些便利的断言方法。**提交用例前请先认真阅读本文档、[Endless 用例编写风格指南][style-guide] ，并熟悉 [Endless 用例 review 流程][review] 。**

<!-- both VSCode/markdown-all-in-one and Emacs/markdown-mode have commands to update the toc -->
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
**Table of Contents**

- [Endless 用户指南](#endless-用户指南)
    - [快速上手](#快速上手)
        - [安装必要工具](#安装必要工具)
        - [调试 “hello world” 用例](#调试-hello-world-用例)
        - [调试一个“接近真实”的用例](#调试一个接近真实的用例)
        - [调试一个“真实”的用例](#调试一个真实的用例)
        - [编写新测试用例](#编写新测试用例)
        - [测试用例示例](#测试用例示例)
    - [使用本地资源调试](#使用本地资源调试)
        - [教程](#教程)
        - [示例](#示例)
    - [负载编写与运行](#负载编写与运行)
    - [集群操作和错误注入](#集群操作和错误注入)
        - [Chaos](#chaos)
        - [Failpoint](#failpoint)
    - [断言（校验）](#断言校验)
        - [基于 PromQL 的断言](#基于-promql-的断言)
        - [在 goroutine 中进行断言](#在-goroutine-中进行断言)
    - [资源申请与查看](#资源申请与查看)
        - [常见的 testbed.yaml 示例](#常见的-testbedyaml-示例)
    - [常见问题](#常见问题)
        - [用例设计、录入、编写调试和运行的标准流程是什么样子？](#用例设计录入编写调试和运行的标准流程是什么样子)
        - [安装 tcctl 失败，遇到 "fatal: could not read Username for 'https://github.com': terminal prompts disabled" 错误](#安装-tcctl-失败遇到-fatal-could-not-read-username-for-httpsgithubcom-terminal-prompts-disabled-错误)
        - [本地连接远端集群时，遇到 "dial tcp: lookup tc-tidb.testbed-endless-xxx.svc on 1.1.1.1: no such host" 错误](#本地连接远端集群时遇到-dial-tcp-lookup-tc-tidbtestbed-endless-xxxsvc-on-1111-no-such-host-错误)
            - [手动配置代理](#手动配置代理)
        - [我明明只执行了用例 A，但用例 B 的逻辑也执行了？](#我明明只执行了用例-a但用例-b-的逻辑也执行了)
- [Running Suite: Examples Suite](#running-suite-examples-suite)

<!-- markdown-toc end -->

## 快速上手

一般来说，编写调试一个测试用例分为几步：

1. 创建测试资源
2. 编写测试逻辑
3. 本地调试

你可以先尝试在本地调试几个简单的测试用例。

> :memo: 如果你对测试用例的设计和日常运行等流程有疑问，或者对各个角色的分工有疑惑，
> 你可以参考 FAQ 章节的 “用例设计、录入、编写调试和运行的标准流程” 部分。

### 安装必要工具

**第一步**：克隆项目并进入项目的根目录，运行如下命令以安装必要的开发调试工具：
ginkgo 和 tcctl。
```sh
$ git clone git@github.com:pingcap/endless.git
$ cd endless
$ make dev_tools
```
`tools/bin/ginkgo -h` 和 `tools/bin/tcctl -h` 可以成功运行，则代表工具成功安装成功。

这两个工具会在测试编写和调试过程中会频繁使用，建议你安装它们并添加到 PATH 中。
```sh
$ go install github.com/onsi/ginkgo/ginkgo@latest
$ GOPRIVATE=github.com/pingcap go install github.com/pingcap/test-infra/tools/tcctl@latest
```

注：
1. 由于 Endless 和 test-infra 是私有项目，在执行部分 make 命令时需要指定 GOPRIVATE 环境变量。
   否则会遇到类似这样的错误：fatal: could not read Username for ...: terminal prompts disabled
2. 如果你的上述安装过程没有报错，但运行 `ginkgo -h` 时遇到 `Command not found` 错误，
   你需要检查你是否正确配置了 PATH 环境变量。通常来说，你将 `~/go/bin` 目录加入到 PATH
   中即可，相关细节可以参考 [How to Write Go Code](https://golang.org/doc/code)。
3. 如果遇到类似的错误： `Username for 'https://github.com'` 和 `remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead`，可以考虑执行 `git config --global url."git@github.com:".insteadOf "https://github.com/"` 或在 ~/.gitconfig 添加 
```
[url "git@github.com:"]
    insteadOf = https://github.com/
```

### 调试 “hello world” 用例

`testcase/helloworld` 目录已经有几个编写好的用例，你可以先尝试运行 [`hello_world_1`][hello-world-1]。
运行如下命令以调试该用例：

```sh
$ cd testcase/helloworld/
$ ginkgo -focus hello_world_1
```
你理应可以看到类似如下的输出。
```
Running Suite: Helloworld Suite
===============================
Random Seed: 1627980577
Will run 1 of 3 specs

•SS
Ran 1 of 3 Specs in 0.000 seconds
SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 2 Skipped
PASS

Ginkgo ran 1 suite in 3.495893582s
Test Suite Passed
```
恭喜你，你已经成功的运行了第一个用例。用例代码可以看：

### 调试一个“接近真实”的用例

在实际编写测试用例的时候，你通常需要创建一个 TiDB 集群用于测试，
下面将演示如何调试一个需要 TiDB 集群的测试用例。

你可以使用 `tcctl` 工具创建 TiDB 集群。使用 tcctl 工具前，你需要在
[token 申请页面][tcml-token] 生成一个 token。
```
$ tcctl config --token=<your-token>
$ cd testcase/helloworld/
$ tcctl testbed create -f testbed.yaml -r http://rms-staging.pingcap.net:30007
```
创建一个规模不大的集群通常在 5 分钟以内。集群创建完毕后，tcctl 工具会将集群信息保存在当前目录的
`.env` 文件中。同时，它也会生成一个 `kubeconfig.yml` 文件，它是访问该集群的凭证，该文件的详细作用可以参考
[使用 kubeconfig 文件组织集群访问](https://kubernetes.io/zh/docs/concepts/configuration/organize-cluster-access-kubeconfig/)。
接下来，你可以尝试运行测试用例 [`hello_world_2`][hello-world-2]

```
$ cd testcase/helloworld/
$ ginkgo -focus hello_world_2
```
你理应可以看到类似如下的输出。
```
~/c/e/t/helloworld docs* > ginkgo -focus hello_world_2
Running Suite: Helloworld Suite
===============================
Random Seed: 1628129235
Will run 1 of 3 specs

•SS
Ran 1 of 3 Specs in 0.093 seconds
SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 2 Skipped
PASS

Ginkgo ran 1 suite in 4.304349248s
Test Suite Passed
```

### 调试一个“真实”的用例

[`hello_world_3`][hello-world-3] 第三个用例会连接到 TiDB 集群，执行若干条 SQL 语句。
值得注意的是，由于 TiDB 集群是部署在 K8s 上，而目前 K8s 的内部网络和本地是不直接相通的，
该用例在执行时，它会给自己设置网络代理相关的环境变量，从而访问集群。

```
$ ginkgo -focus="hello_world_3"
Running Suite: Helloworld Suite
===============================
Random Seed: 1628130036
Will run 1 of 3 specs

SS•
Ran 1 of 3 Specs in 2.063 seconds
SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 2 Skipped
PASS

Ginkgo ran 1 suite in 5.487582312s
Test Suite Passed
```

至此，你已经了解了如何调试一个现有的用例。你可以开始编写自己的测试用例了！

### 编写新测试用例

一个 go 的 pacakge 就是一个用例集，一般来说也对应一个文件夹，比如上面我们用到的是
helloworld 用例集，里面有若干个用例。当你需要新建一个用例集，一个简单的方法是
拷贝 helloworld 用例集的目录。

```sh
# 拷贝目录
$ cd testcase/
$ cp -r helloworld/ myown
```

当然，你最好将 package 名字（`helloworld_test`） 和Suite（`Helloworld`）
修改成你想要的。

```sh
$ cd myown/

# Linux 系统，执行该命令以修改包名
$ git grep --untracked -l 'helloworld_test' | xargs sed -i 's/helloworld_test/myown_test/g'
$ git grep --untracked -l 'Helloworld' | xargs sed -i 's/Helloworld/Myown/g'

# macOS 系统，执行该命令
$ git grep --untracked -l 'helloworld_test' | xargs sed -i '' -e 's/helloworld_test/myown_test/g'
$ git grep --untracked -l 'Helloworld' | xargs sed -i '' -e 's/Helloworld/Myown/g'
```

你可以尝试执行 myown 用例集中的 `hello_world_1` 用例以确保上述操作是正确无误的。
```
$ cd myown/
$ ginkgo -focus="hello_world_1"
```

### 测试用例示例

如果你想编写随机/长稳类型的测试用例，建议参考 `testcase/examples/sample_test.go`。
如果用例属于确定型，可以参考 attributes 的测试用例来编写。你还可以在
[examples][examples] 目录中找到各个模块的使用示例。

## 使用本地资源调试

如果你完整的走了一遍“快速上手”流程，你会发现测试用例的运行需要依赖外部（K8s）的资源，
测试用例通过 SDK 来访问资源。K8s 资源有一些优势，比如它可以方便的进行升级、错误注入等，
但它也有一些不足，比如它本身比较复杂，可控制性比较弱，有一定的学习成本。

很多测试用例其实只依赖 TiDB 对外暴露的服务，而不需要扩缩容、升级、错误注入等复杂操作。
对于这类用例，你可以使用本地资源来调试测试用例的代码。目前 SDK 对部分资源做了本地支持。
当前，SDK 支持的本地资源类型有 TiDBCluster 和 WorkloadNode。在 `testcase/hellolocal`
目录下有一个示例，它可以使用本地资源来运行。

### 教程

经过之前的实践你应该知道 SDK 会从环境变量中读取资源句柄，而这些环境变量，一般都是保存在
`.env` 文件中。下面是一个 .env 文件的示例：

```sh
# This file was generated by tcctl testbed create
# GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
TI_PARAM_RES_tc: {"provider":"github.com/pingcap/test-infra/sdk/resource/impl/k8s.TiDBClusterClient","name":"tc","testbed":"testbed-cosven-chb8f","type":"TIDB_CLUSTER","ready":true}
TI_PARAM_RES_go-tpc: {"provider":"github.com/pingcap/test-infra/sdk/resource/impl/k8s.WorkloadNodeClient","name":"go-tpc","testbed":"testbed-cosven-chb8f","type":"WORKLOAD_NODE","ready":true}
HTTP_PROXY: socks5://rms-staging.pingcap.net:30081
MYSQL_PROXY: socks5h://rms-staging.pingcap.net:30081
GRPC_PROXY: socks5h://rms-staging.pingcap.net:30081
```

前缀为 `TI_PARAM_RES_` 的环境变量代表资源句柄，环境变量的后半部分是是资源的名字，这里，
它的名字是 `tc` 和 `go-tpc`。资源句柄使用一个 JSON 结构来表示，它有两个值得注意的的字段。
`provider` 意为资源提供方，默认是 K8s 提供，`name` 是资源名字。后缀为 `PROXY`
的环境变量的值是用于访问资源的网络代理地址。

修改资源句柄的内容，你即可使用本地资源。假设你通过 `tiup playground` 在本地部署了一个
TiDB 集群。你可以自行编写一个 `.env` 文件，举个例子
```sh
TI_PARAM_RES_tc1: {"provider":"github.com/pingcap/test-infra/sdk/resource/impl/local.TiDBCluster", "name": "tc1","type":"TIDB_CLUSTER","ready":true}
TI_PARAM_RES_tc1_TIDB_URL: ["mysql://root:@127.0.0.1:4000"]
TI_PARAM_RES_tc1_TIKV_URL: ["http://127.0.0.1:20160"]
TI_PARAM_RES_tc1_PD_URL: ["http://127.0.0.1:2379"]
TI_PARAM_RES_tc1_TIFLASH_URL: ["127.0.0.1:3930"]
TI_PARAM_RES_tc1_PROMETHEUS_URL: ["http://127.0.0.1:9090"]

TI_PARAM_RES_gotpc1: {"provider":"github.com/pingcap/test-infra/sdk/resource/impl/local.WorkloadNode", "name": "gotpc1","type":"WORKLOAD_NODE","ready":true}
```
上述文件内容相比之前，它主要有几个改动点
1. 资源句柄的 `provider` 字段有变化，之前资源提供方是 `k8s`，这个文件中改成了 `local`。
2. 这个文件需要显示的声明你部署的 TiDB 集群的各个服务地址，比如 TiDB_URL。

除了编写 `.env` 文件外，你还需要在测试用例代码中引入一个 local 包。通常，
你可以在 `*_suite_test.go` 文件中来引入这些包。
```golang
import (
    _ "github.com/pingcap/test-infra/sdk/resource/impl/local"
    _ "github.com/pingcap/test-infra/sdk/resource/impl/k8s"
)
```

### 示例

你可以运行 `testcase/examples/hellolocal` 目录中的用例来尝试这项特性
1. 进入 hellolocal 目录，并将 `local.env` 文件更名为 `.env`。
2. 运行 `ginkgo -v --focus="hello_local_1"` 即可体验 `local.WorkloadNode`。
3. 运行 `ginkgo -v --focus="hello_local_2"` 便可以体验 `local.TiDBCluster`。

## 负载编写与运行

暂无最佳实践。

## 集群操作和错误注入

### Chaos

暂时请参考 `pkg/chaos` 的代码注释。

### Failpoint

使用 failpoint 需要专门的镜像，TiDB 和 PD 还需要设置环境变量才能够使用，`testbed.yaml` 需要做如下修改：

```yaml
items:
  - name: tc
    type: TIDB_CLUSTER
    spec:
      version: "master-failpoint"
      pd:
        baseImage: hub-new.pingcap.net/qa/pd
        env:
          - name: GO_FAILPOINTS
            value: github.com/tikv/pd/server/api/enableFailpointAPI=return
        ...
      tidb:
        baseImage: hub-new.pingcap.net/qa/tidb
        env:
          - name: GO_FAILPOINTS
            value: github.com/pingcap/tidb/server/enableTestAPI=return
        ...
      tikv:
        baseImage: hub-new.pingcap.net/qa/tikv
        ...
```

然后你只需要通过 `fail.FailClient` 就可以开启或关闭对应的 failpoint，例如：

```go
import "github.com/pingcap/endless/pkg/client/fail"

func useFailpoint() {
	failClient := fail.NewFailClient(tc, resource.TiDB)
	failpointPath := "tikvclient/beforeCommit"
	failpointAction := "sleep(1000)"
	// enable
	failClient.Enable(failpointPath, failpointAction)
	// disable
	failClient.Disable(failpointPath)
}

```

## 断言（校验）

### 基于 PromQL 的断言

在编写性能、稳定性测试时，用例经常需要根据 TiDB 监控指标进行断言。比如断言每秒请求数
跌幅不能超过 50%，各 TiKV 实例的切片数需要均衡。你可以使用 `pkg/matcher/metrics`
包中的断言函数来实现上述需求。举个例子
```golang
Consistently(tidbcluster).ShouldNot(QPSDropMoreThan(0.5))
```

**值得注意的是**，在编写断言函数时，如果你在 PromQL 语句中使用到了 `rate` 函数，
你通常需要将其区间设置为 1 分钟以上，否则该语句可能会计算失败，结果返回值为空。
你可以参考 [What range should I use with rate()?][promql-rate-range] 文章以获取细节。

**令一点需要注意的是**，在 [#393](https://github.com/pingcap/endless/pull/393) 合并后，我们为
`PromQLEvaluatedToEmpty` 和 `TolerateByDuration` 两个 matcher 引入了更加简洁易读的结果输出（Diagnose Info，具体效果请参见该 PR），而它们的默认日志输出级别均降为 Debug。在编写和调试测试用例时，你可能需要开启它们的日志输出以获得更即时的反馈：你可以通过将 `ENDLESS_LOG_LEVEL` 环境变量设为 `debug` 以开启 Debug 级日志输出。

### 在 goroutine 中进行断言

在 goroutine 中进行断言需要注意几个问题：

1. 根据 [ginkgo 文档][ginkgo-docs] 建议，当你在 goroutine 中进行断言时，
   你应该使用 `GinkgoRecover`，否则，当断言失败，测试程序会异常退出，其它测试不会继续执行。
2. 在 goroutine 里面发生断言失败时，整个用例（It/TestCase 块）不会立即失败，
   它会等这个块主流程全部走完才会失败。并且，其它 goroutine 不会立即停止。

Endless 提供了 `WithGinkgoRecover` 工具函数，将它与 [errgroup][errgroup]
一起使用可实现 fastfail，可以较好的解决 “在 goroutine 中断言”的问题。
你可以在 testcase/examples 中找到使用示例。

## 资源申请与查看

你可以通过 test-infra 的[资源管理系统（RMS）][rms]在远程创建测试所需要的资源，比如 TiDB 集群。
你可以编写 testbed.yaml 来描述你的资源需求，然后使用 tcctl 工具来创建资源。

### 常见的 testbed.yaml 示例

Endless 在 [examples][examples] 目录中存放了一些常见的 testbed.yaml 示例。

- *testbed_minimal.yaml* : 该配置中描述了一个 1PD 1TiDB 3TiKV 的集群，
  它只会消耗最小量的资源。如果你的测试用例对集群性能无特殊要求，建议使用该配置。
  比如：纯 SQL 类用例。
- *testbed_standard.yaml* ：标准的测试集群配置。对集群稳定性、可用性有要求的测试用例，
  建议使用该配置。比如：TiKV 稳定性相关的测试用例。
- ...

## 常见问题

### 用例设计、录入、编写调试和运行的标准流程是什么样子？

![flow-image](https://user-images.githubusercontent.com/4962134/128994748-cdae746b-2e28-4b6f-976d-9e69f055520e.png)

### 安装 tcctl 失败，遇到 "fatal: could not read Username for 'https://github.com': terminal prompts disabled" 错误

由于 tcctl 代码是在私有仓库 `github.com/pingcap/test-infra` 中，使用 `go install`
安装时容易遇到如下错误：
```
$ go install github.com/pingcap/test-infra/tools/tcctl@latest
go: downloading github.com/pingcap/test-infra/tools/tcctl v0.0.0-20210805104846-6061cd2532b7
go: downloading github.com/pingcap/test-infra v0.0.0-20210805104846-6061cd2532b7
go install github.com/pingcap/test-infra/tools/tcctl@latest: github.com/pingcap/test-infra/tools/tcctl@v0.0.0-20210805104846-6061cd2532b7: verifying module: github.com/pingcap/test-infra/tools/tcctl@v0.0.0-20210805104846-6061cd2532b7: reading https://sum.golang.org/lookup/github.com/pingcap/test-infra/tools/tcctl@v0.0.0-20210805104846-6061cd2532b7: 410 Gone
        server response:
        not found: github.com/pingcap/test-infra/tools/tcctl@v0.0.0-20210805104846-6061cd2532b7: invalid version: git fetch -f origin refs/heads/*:refs/heads/* refs/tags/*:refs/tags/* in /tmp/gopath/pkg/mod/cache/vcs/1e33ab0ef89b73f1699e584cdeabf76ac461b3fa81bafd2d6f940a0f5ab38a03: exit status 128:
                fatal: could not read Username for 'https://github.com': terminal prompts disabled
```

你可以参考：https://stackoverflow.com/a/27501039/4302892 方法来解决。

### 本地连接远端集群时，遇到 "dial tcp: lookup tc-tidb.testbed-endless-xxx.svc on 1.1.1.1: no such host" 错误

由于远端集群部署在内部 k8s 集群上，内部 k8s 的网络和 IDC / 办公室网络没有直接连通，
你需要使用代理才能访问。

较新版本的 [tcctl](https://github.com/pingcap/test-infra/tree/main/tools/tcctl) （提示：立即检查并更新你的 tcctl）
支持在 .env 文件中直接输出需要的代理，Endless 提供了 [`AutoConf`](https://github.com/pingcap/endless/blob/master/pkg/util/autoconf.go)
用于自动导入 .env 中由 tcctl 生成的代理信息和其它环境变量。你可以在测试套件的 `BeforeSuite` 块中调用 `AutoConf()` 函数，
一切都会自动完成。具体的示例可以参考
[testcase / examples](https://github.com/pingcap/endless/blob/master/testcase/examples/examples_suite_test.go) 。

**注意以下要点：** 首先是只有较新版本的 tcctl 才支持该功能。

#### 手动配置代理

如果上述 _新版本 tcctl + `AutoConf()`_ 方法不能达到效果，我们希望你首先[提一个 issue](https://github.com/pingcap/endless/issues/new) ！（谢谢！）
在这之后，你可以手工配置使用已经部署好的代理： `socks5://172.16.4.67:30081`。
举个例子，指定 `MYSQL_PROXY`、`HTTP_PROXY` 和 `GRPC_PROXY` 环境变量，这可以解决大部分的网络问题：
```sh
MYSQL_PROXY=socks5://172.16.4.67:30081 HTTP_PROXY=socks5://172.16.4.67:30081 GRPC_PROXY=socks5://172.16.4.67:30081 ./testcase
```

而 "no such host" 错误是指 DNS 解析失败，Golang 部分网络库不能识别类似 `socks5h` 的协议头，
如果你遇到了该类问题，请使用 [proxychains4][proxychains4]。

### 我明明只执行了用例 A，但用例 B 的逻辑也执行了？

举个例子，有一个测试中包含如下测试用例代码

```golang
var _ = ParameterizedDescribe("z", func(flags *flag.FlagSet) {
	println("z: inside describe")
	Context("z", func() {
		println("z: inside context")
		TestCase("z", "z", func() {
			println("z: inside testcase")
			Expect(nil).Should(BeNil())
		})
        TestCase("z2", "z2", func() {
			println("z2: inside testcase")
			Expect(nil).Should(BeNil())
		})
	})
})

var _ = Describe("a", func() {
	println("a: inside describe")
	Context("a", func() {
		println("a: inside context")
		TestCase("a", "a", func() {
			println("a: inside testcase")
			Expect(nil).Should(BeNil())
		})
	})
})
```

通过命令 `ginkgo -focus "#z#"` 执行用例 "z" 的时候，你会看到如下输出。
可以发现，`Describe("a", ...)` 区块的代码也被执行了。这个现象是“符合预期”的，
**无论如何设置 ginkgo 的执行参数，Describe/Context 区块的代码总是会被执行到**。

一般来说，Describe/Context 区块中只包含“变量声明”等逻辑，初始化的逻辑应该放在
BeforeEach 等 block 中。细节可以参考 [用例编写风格指南][style-guide]，
「不要在 Describe、Context 和 When 这些容器块中放置资源获取和初始化逻辑」章节。

```
~/c/e/t/examples fix-examples* > ginkgo -focus "#z#"
z: inside describe
z: inside context
a: inside describe
a: inside context
Running Suite: Examples Suite
=============================
Random Seed: 1630042995
Will run 1 of 9 specs

SSSSSSSS•
z: inside testcase
Ran 1 of 9 Specs in 0.001 seconds
SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 8 Skipped
PASS

Ginkgo ran 1 suite in 3.713848176s
Test Suite Passed
```


[ginkgo]: https://github.com/onsi/ginkgo
[ginkgo-docs]: https://onsi.github.io/ginkgo/
[hello-world-1]: https://github.com/pingcap/endless/blob/a85737609cc23be660130b8431df7568e8b31cda/testcase/helloworld/hello_world_test.go#L12-L18
[hello-world-2]: https://github.com/pingcap/endless/blob/a85737609cc23be660130b8431df7568e8b31cda/testcase/helloworld/hello_world_test.go#L21-L27
[hello-world-3]: https://github.com/pingcap/endless/blob/a85737609cc23be660130b8431df7568e8b31cda/testcase/helloworld/hello_world_test.go#L30-L42
[tcml-token]: https://tcms.pingcap.net/dashboard/settings/tokens
[proxychains4]: https://github.com/haad/proxychains
[style-guide]: ./style-guide.md
[review]: ./review.md
[mysql-test]: https://dev.mysql.com/doc/dev/mysql-server/latest/PAGE_MYSQL_TEST_RUN.html
[test-infra-sdk]: https://github.com/pingcap/test-infra/tree/main/sdk
[examples]: ../testcase/examples
[rms]: https://github.com/pingcap/test-infra#%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86-rms-%E5%92%8C-sdk
[errgroup]: https://pkg.go.dev/golang.org/x/sync/errgroup
[promql-rate-range]: https://www.robustperception.io/what-range-should-i-use-with-rate
