Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get-started: add notice for test parallel with range #138

Merged
merged 8 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions src/contribute-to-tidb/code-style-and-quality-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

This is an attempt to capture the code and quality standard that we want to maintain.


## The newtype pattern improves code quality

We can create a new type using the `type` keyword.

The newtype pattern is perhaps most often used in Golang to get around type restrictions rather than to try to create new ones. It is used to create different interface implementations for a type or to extend a builtin type or a type from an existing package with new methods.

However, it is generally useful to improve code clarity by marking that data has gone through either a validation or a transformation. Using a different type can reduce error handling and prevent improper usage.
However, it is generally useful to improve code clarity by marking that data has gone through either a validation or a transformation. Using a different type can reduce error handling and prevent improper usage.

```go
package main
Expand Down Expand Up @@ -62,3 +61,94 @@ As a rule of thumb is that when a struct has 10 or more words we should use poin
* method size: small inlineable methods favor value receivers.
* Is the struct called repeatedly in a for loop? This favors pointer receivers.
* What is the GC behavior of the rest of the program? GC pressure may favor value receivers.

## Some tips for range and goroutines

There are 2 types of range, `with index` and `without index`. Let's see an example for range `with index`.

```go
func TestRangeWithIndex(t *testing.T) {
rows := []struct{ index int }{{index: 0}, {index: 1}, {index: 2}}
for _, row := range rows {
row.index += 10
}
for i, row := range rows {
require.Equal(t, i+10, row.index)
}
}
```

the output is:

```shell
Error Trace: version_test.go:39
Error: Not equal:
expected: 10
actual : 0
Test: TestShowRangeWithIndex
```

Above test fails since when range `with index`, the loop iterator variable is `the same instance of the variable` with `a clone of iteration target value`.

### The same instance of the variable

Since the the loop iterator variable is `the same instance of the variable`, it may result in hard to find error when use with goroutines.

```go
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
for _ = range values {
<-done
}
```

You might mistakenly expect to see a, b, c as the output, but you'll probably see instead is c, c, c.
This is because each iteration of the loop uses the same instance of the variable v, so each closure shares that single variable.

This is the same reason which result wrong test when use `t.Parallel()` with range, which is covered in `parallel` section of [write-and-run-unit-tests](../get-started/write-and-run-unit-tests.md)

### A clone of iteration target value

Since the loop iterator variable is `a clone of iteration target value`, it may result in logic error if you do not pay attention. It can also lead to performance issue compared with `without index` range or `for` loop.

```go
type Item struct {
id int
value [1024]byte
}

func BenchmarkRangeIndexStruct(b *testing.B) {
var items [1024]Item
for i := 0; i < b.N; i++ {
var tmp int
for k := range items {
tmp = items[k].id
}
_ = tmp
}
}

func BenchmarkRangeStruct(b *testing.B) {
var items [1024]Item
for i := 0; i < b.N; i++ {
var tmp int
for _, item := range items {
tmp = item.id
}
_ = tmp
}
}
```

```shell
BenchmarkRangeIndexStruct-12 4875518 246.0 ns/op
BenchmarkRangeStruct-12 16171 77523 ns/op
```

You can see range `with index` is much slower than range `without index`, since range `with index` use cloned value so have big performance decrease if `iteration target` is a large struct which use a lot of memory.
17 changes: 17 additions & 0 deletions src/get-started/write-and-run-unit-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ func TestParent(t *testing.T) {

Generally, if a test modifies global configs or fail points, it should be run in serial.

When use parallel with range, you need be careful.

In Golang, the loop iterator variable is a single variable that takes different value in each loop iteration, so there is a very good chance that when you run this code you will see the last element used for every iteration. You may use below paradigm to work around.

```go
func TestParallelWithRange(t *testing.T) {
for _, tc := range tests {
// copy iterator variable into a new variable, see issue #27779 in tidb repo
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
...
})
}
}
```

### Test kits

Most of our tests are much more complex than what we describe above. For example, to set up a test, we may create a mock storage, a mock session, or even a local database instance.
Expand Down