golang 基准测试和性能测试总结
1、测试类型
在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
| 类型 | 格式 | 作用 |
|---|---|---|
| 测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
| 基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
| 示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
2、测试函数
一、测试函数的格式
每个测试函数必须导入testing包,测试函数的基本格式如下:
func TestName(t *testing.T){// ...}
测试函数的名字必须以Test开头,可选函数的后缀名必须以大写字母开头
func TestAdd(t *testing.T){ ... }func TestSum(t *testing.T){ ... }func TestLog(t *testing.T){ ... }
其中参数t用于报告测试失败和附加的日志信息,testing.T拥有的方法如下
func (c *T) Error(args ...interface{})func (c *T) Errorf(format string, args ...interface{})func (c *T) Fail()func (c *T) FailNow()func (c *T) Failed() boolfunc (c *T) Fatal(args ...interface{})func (c *T) Fatalf(format string, args ...interface{})func (c *T) Log(args ...interface{})func (c *T) Logf(format string, args ...interface{})func (c *T) Name() stringfunc (t *T) Parallel()func (t *T) Run(name string, f func(t *T)) boolfunc (c *T) Skip(args ...interface{})func (c *T) SkipNow()func (c *T) Skipf(format string, args ...interface{})func (c *T) Skipped() bool
二、示例
tree.├── split.go└── split_test.go0 directories, 2 files
Split.go/*** @Author: zhangsan* @Description:* @File: split* @Version: 1.0.0* @Date: 2021/1/11 上午11:19*/package testCodeimport "strings"func Split(s, sep string) (result []string) {i := strings.Index(s, sep)for i > -1 {result = append(result, s[:i])s = s[i+1:]i = strings.Index(s, sep)}result = append(result, s)return}split_test.go/*** @Author: zhangsan* @Description:* @File: split_test.go* @Version: 1.0.0* @Date: 2021/1/11 上午11:20*/package testCode// split/split_test.goimport ("reflect""testing")func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数got := Split("a:b:c",":") // 程序输出的结果want := []string{"a", "b", "c"} // 期望的结果if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示}}
结果
-> % time go testPASSok test/testCode 0.007sgo test 0.42s user 0.26s system 148% cpu 0.460 total
A、 查看test详情 -v
/*** @Author: zhangsan* @Description:* @File: split_test.go* @Version: 1.0.0* @Date: 2021/1/11 上午11:20*/package testCode// split/split_test.goimport ("reflect""testing")func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数got := Split("a:b:c",":") // 程序输出的结果want := []string{"a", "b", "c"} // 期望的结果if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示}}func TestMoreSplit(t *testing.T) {got := Split("abcd", "bc")want := []string{"a", "d"}if !reflect.DeepEqual(want, got) {t.Errorf("excepted:%v, got:%v", want, got)}}
结果
-> % time go test -v=== RUN TestSplit--- PASS: TestSplit (0.00s)=== RUN TestMoreSplitTestMoreSplit: split_test.go:31: excepted:[a d], got:[a cd]--- FAIL: TestMoreSplit (0.00s)FAILexit status 1FAIL test/testCode 0.007sgo test -v 0.40s user 0.21s system 162% cpu 0.375 total
B、指定test的函数 -run TestXxx
time go test -v -run TestMoreSplit=== RUN TestMoreSplitTestMoreSplit: split_test.go:31: excepted:[a d], got:[a cd]--- FAIL: TestMoreSplit (0.00s)FAILexit status 1FAIL test/testCode 0.010sgo test -v -run TestMoreSplit 0.46s user 0.53s system 113% cpu 0.875 total
C、执行子测试 t.run
time go test -v=== RUN TestSplit=== RUN TestSplit/simple=== RUN TestSplit/wrong_sep=== RUN TestSplit/more_sepTestSplit/more_sep: split_test.go:34: excepted:[]string{"a", "d"}, got:[]string{"a", "cd"}=== RUN TestSplit/leading_sepTestSplit/leading_sep: split_test.go:34: excepted:[]string{"河有", "又有河"}, got:[]string{"", "\xb2\x99河有", "\xb2\x99又有河"}--- FAIL: TestSplit (0.00s)--- PASS: TestSplit/simple (0.00s)--- PASS: TestSplit/wrong_sep (0.00s)--- FAIL: TestSplit/more_sep (0.00s)--- FAIL: TestSplit/leading_sep (0.00s)FAILexit status 1FAIL test/testCode 0.011sgo test -v 0.34s user 0.18s system 172% cpu 0.299 total
D、执行指定子测试 -run=Split/simple
time go test -v -run=Split/simple-> % time go test -v -run=Split/simple=== RUN TestSplit=== RUN TestSplit/simple--- PASS: TestSplit (0.00s)--- PASS: TestSplit/simple (0.00s)PASSok test/testCode 0.008sgo test -v -run=Split/simple 0.41s user 0.25s system 164% cpu 0.404 total3、测试覆盖率 -cover
在测试中至少被运行一次的代码占总代码的比例。
go test -cover来查看测试覆盖率。例如:
time go test -v -cover=== RUN TestSplit=== RUN TestSplit/simple=== RUN TestSplit/wrong_sep=== RUN TestSplit/more_sep--- PASS: TestSplit (0.00s)--- PASS: TestSplit/simple (0.00s)--- PASS: TestSplit/wrong_sep (0.00s)--- PASS: TestSplit/more_sep (0.00s)PASScoverage: 100.0% of statementsok test/testCode 0.005sgo test -v -cover 0.41s user 0.26s system 163% cpu 0.412 total
Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:
-coverprofile=c.out
time go test -v -cover -coverprofile=c.out=== RUN TestSplit=== RUN TestSplit/simple=== RUN TestSplit/wrong_sep=== RUN TestSplit/more_sep--- PASS: TestSplit (0.00s)--- PASS: TestSplit/simple (0.00s)--- PASS: TestSplit/wrong_sep (0.00s)--- PASS: TestSplit/more_sep (0.00s)PASScoverage: 100.0% of statementsok test/testCode 0.014sgo test -v -cover -coverprofile=c.out 0.47s user 0.49s system 115% cpu 0.825 total
查看输出的文件
每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。
go tool cover -html=c.out
4、基准测试--性能
基本参数说明
-run 知道单次测试,一般用于代码逻辑验证
-bench=. 执行所有 Benchmark,也可以通过用例函数名来指定部分测试用例
-benchtime 指定测试执行时长
-cpuprofile 输出 cpu 的 pprof 信息文件
-memprofile 输出 heap 的 pprof 信息文件。
-blockprofile 阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
-mutexprofile 互斥锁分析,报告互斥锁的竞争情况
benchmark 测试用例常用函数
b.ReportAllocs() 输出单次循环使用的内存数量和对象 allocs 信息
b.RunParallel() 使用协程并发测试
b.SetBytes(n int64) 设置单次循环使用的内存数量
一、基准测试的格式
基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:
func BenchmarkName(b *testing.B){// ...}
基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B拥有的方法如下:
func (c *B) Error(args ...interface{})func (c *B) Errorf(format string, args ...interface{})func (c *B) Fail()func (c *B) FailNow()func (c *B) Failed() boolfunc (c *B) Fatal(args ...interface{})func (c *B) Fatalf(format string, args ...interface{})func (c *B) Log(args ...interface{})func (c *B) Logf(format string, args ...interface{})func (c *B) Name() stringfunc (b *B) ReportAllocs()func (b *B) ResetTimer()func (b *B) Run(name string, f func(b *B)) boolfunc (b *B) RunParallel(body func(*PB))func (b *B) SetBytes(n int64)func (b *B) SetParallelism(p int)func (c *B) Skip(args ...interface{})func (c *B) SkipNow()func (c *B) Skipf(format string, args ...interface{})func (c *B) Skipped() boolfunc (b *B) StartTimer()func (b *B) StopTimer()
二、示例
基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:
time go test -bench=Splitgoos: darwingoarch: amd64pkg: test/testCode5007372 248 ns/opPASSok test/testCode 1.491sgo test -bench=Split 2.02s user 0.65s system 106% cpu 2.505 total
BenchmarkSplit-4表示对Split函数进行基准测试,数字4表示GOMAXPROCS的值,这个对于并发基准测试很重要。
5007372和248ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值。
B、查看内存分配情况 -benchmem
time go test -bench=Split -benchmemgoos: darwingoarch: amd64pkg: test/testCode5031902 245 ns/op 112 B/op 3 allocs/opPASSok test/testCode 1.485sgo test -bench=Split -benchmem 1.99s user 0.57s system 110% cpu 2.327 total
112 B/op表示每次操作内存分配了112字节
3 allocs/op则表示每次操作进行了3次内存分配`
我们将我们的Split`函数优化如下:
func Split(s, sep string) (result []string) {result = make([]string, 0, strings.Count(s, sep)+1)i := strings.Index(s, sep)for i > -1 {result = append(result, s[:i])s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度i = strings.Index(s, sep)}result = append(result, s)return}
这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:
time go test -bench=Split -benchmemgoos: darwingoarch: amd64pkg: test/testCode8347236 148 ns/op 48 B/op 1 allocs/opPASSok test/testCode 1.397sgo test -bench=Split -benchmem 1.90s user 0.64s system 108% cpu 2.345 total
这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:
C、性能比较函数
上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下:
func benchmark(b *testing.B, size int){/* ... */}func Benchmark10(b *testing.B){ benchmark(b, 10) }func Benchmark100(b *testing.B){ benchmark(b, 100) }func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
例如我们编写了一个计算斐波那契数列的函数如下:
// fib.go// Fib 是一个计算第n个斐波那契数的函数func Fib(n int) int {if n < 2 {return n}return Fib(n-1) + Fib(n-2)}编写的性能比较函数// fib_test.gofunc benchmarkFib(b *testing.B, n int) {for i := 0; i < b.N; i++ {Fib(n)}}func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) }func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
运行基准测试
split $ go test -bench=.goos: darwingoarch: amd64pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib1000000000 2.03 ns/op300000000 5.39 ns/op200000000 9.71 ns/op5000000 325 ns/op30000 42460 ns/op2 638524980 ns/opPASSok github.com/Q1mi/studygo/code_demo/test_demo/fib 12.944s
这里需要注意的是,默认情况下,==每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。==
最终的BenchmarkFib40只运行了两次,每次运行的平均值只有不到一秒。像这种情况下我们应该可以使用-benchtime标志增加最小基准时间,以产生更准确的结果。例如:
D、指定执行时间 -benchtime
time go test -bench=Fib40 -benchtime=20sgoos: darwingoarch: amd64pkg: test/testCodeBenchmarkFib40-4 1000000000 0.000000 ns/opPASSok test/testCode 0.009sgo test -bench=Fib40 -benchtime=20s 0.41s user 0.24s system 159% cpu 0.411 total
E、注意
使用性能比较函数做测试的时候一个容易犯的错误就是把b.N作为输入的大小,例如以下两个例子都是错误的示范:
// 错误示范1func BenchmarkFibWrong(b *testing.B) {for n := 0; n < b.N; n++ {Fib(n)}}// 错误示范2func BenchmarkFibWrong2(b *testing.B) {Fib(b.N)
F、使用协程并发测试
b.RunParallel() 使用协程并发测试
三、重置时间 b.ResetTimer
b.ResetTimer之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:
func BenchmarkSplit(b *testing.B) {time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作b.ResetTimer() // 重置计时器for i := 0; i < b.N; i++ {Split("沙河有沙又有河", "沙")}}
四、协程执行测试 RunParallel
func (b *B) RunParallel(body func(*PB))会以并行的方式执行给定的基准测试。
RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行, 其中goroutine数量的默认值为GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel之前调用SetParallelism 。RunParallel通常会与-cpu标志一同使用。
/*** @Author: zhangsan* @Description:* @File: split_test.go* @Version: 1.0.0* @Date: 2021/1/11 上午11:20*/package testCode// split/split_test.goimport ("testing""time")// fib_test.gofunc BenchmarkSplitParallel(b *testing.B) {b.SetParallelism(3) // 设置使用的CPU数b.RunParallel(func(pb *testing.PB) {for pb.Next() {Split("沙河有沙又有河", "沙")}})}func BenchmarkSplit(b *testing.B) {time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作b.ResetTimer() // 重置计时器for i := 0; i > b.N; i++ {Split("沙河有沙又有河", "沙")}}
% time go test -bench=.goos: darwingoarch: amd64pkg: test/testCode14753401 90.9 ns/op1000000000 0.000000 ns/opPASSok test/testCode 31.447sgo test -bench=. 4.48s user 0.69s system 15% cpu 32.325 total
还可以通过在测试命令后添加-cpu参数如go test -bench=. -cpu 1来指定使用的CPU数量。
4.1 查看内存分配情况ReportAllocs
/*** @Author: zhangsan* @Description:* @File: split_test.go* @Version: 1.0.0* @Date: 2021/1/11 上午11:20*/package testCode// split/split_test.goimport ("testing""time")// fib_test.gofunc BenchmarkSplitParallel(b *testing.B) {b.SetParallelism(3) // 设置使用的CPU数b.RunParallel(func(pb *testing.PB) {for pb.Next() {Split("沙河有沙又有河", "沙")}})b.ReportAllocs()}func BenchmarkSplit(b *testing.B) {time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作b.ResetTimer() // 重置计时器for i := 0; i > b.N; i++ {Split("沙河有沙又有河", "沙")}}time go test -bench=.goos: darwingoarch: amd64pkg: test/testCodeBenchmarkSplitParallel-4 15367134 70.3 ns/op 48 B/op 1 allocs/opBenchmarkSplit-4 1000000000 0.000001 ns/opPASSok test/testCode 31.192sgo test -bench=. 4.50s user 0.99s system 16% cpu 32.559 total
5、Setup与TearDown
6、TestMain初始化
通过在*_test.go文件中定义TestMain函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
如果测试文件包含函数:func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m),然后再运行具体测试。TestMain运行在主goroutine中, 可以在调用 m.Run前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit。
一个使用TestMain来设置Setup和TearDown的示例如下:
func TestMain(m *testing.M) {fmt.Println("write setup code here...") // 测试之前的做一些设置// 如果 TestMain 使用了 flags,这里应该加上flag.Parse()retCode := m.Run() // 执行测试fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作os.Exit(retCode) // 退出测试}
需要注意的是:在调用TestMain时, flag.Parse并没有被调用。所以如果TestMain 依赖于command-line标志 (包括 testing 包的标记), 则应该显示的调用flag.Parse。
7、子测试的Setup与Teardown
有时候我们可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。下面我们定义两个函数工具函数如下:
// 测试集的Setup与Teardownfunc setupTestCase(t *testing.T) func(t *testing.T) {t.Log("如有需要在此执行:测试之前的setup")return func(t *testing.T) {t.Log("如有需要在此执行:测试之后的teardown")}}// 子测试的Setup与Teardownfunc setupSubTest(t *testing.T) func(t *testing.T) {t.Log("如有需要在此执行:子测试之前的setup")return func(t *testing.T) {t.Log("如有需要在此执行:子测试之后的teardown")}}
用法
/*** @Author: zhangsan* @Description:* @File: split_test.go* @Version: 1.0.0* @Date: 2021/1/11 上午11:20*/package testCode// split/split_test.goimport ("reflect""testing")// fib_test.gofunc TestSplit(t *testing.T) {type test struct { // 定义test结构体input stringsep stringwant []string}tests := map[string]test{ // 测试用例使用map存储"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},}teardownTestCase := setupTestCase(t) // 测试之前执行setup操作defer teardownTestCase(t) // 测试之后执行testdoen操作for name, tc := range tests {t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作defer teardownSubTest(t) // 测试之后执行testdoen操作got := Split(tc.input, tc.sep)if !reflect.DeepEqual(got, tc.want) {t.Errorf("excepted:%#v, got:%#v", tc.want, got)}})}}// 测试集的Setup与Teardownfunc setupTestCase(t *testing.T) func(t *testing.T) {t.Log("如有需要在此执行:测试之前的setup")return func(t *testing.T) {t.Log("如有需要在此执行:测试之后的teardown")}}// 子测试的Setup与Teardownfunc setupSubTest(t *testing.T) func(t *testing.T) {t.Log("如有需要在此执行:子测试之前的setup")return func(t *testing.T) {t.Log("如有需要在此执行:子测试之后的teardown")}}-> % go test -v=== RUN TestSplitTestSplit: split_test.go:51: 如有需要在此执行:测试之前的setup=== RUN TestSplit/simpleTestSplit/simple: split_test.go:59: 如有需要在此执行:子测试之前的setupTestSplit/simple: split_test.go:61: 如有需要在此执行:子测试之后的teardown=== RUN TestSplit/wrong_sepTestSplit/wrong_sep: split_test.go:59: 如有需要在此执行:子测试之前的setupTestSplit/wrong_sep: split_test.go:61: 如有需要在此执行:子测试之后的teardown=== RUN TestSplit/more_sepTestSplit/more_sep: split_test.go:59: 如有需要在此执行:子测试之前的setupTestSplit/more_sep: split_test.go:61: 如有需要在此执行:子测试之后的teardown=== RUN TestSplit/leading_sepTestSplit/leading_sep: split_test.go:59: 如有需要在此执行:子测试之前的setupTestSplit/leading_sep: split_test.go:61: 如有需要在此执行:子测试之后的teardownTestSplit: split_test.go:53: 如有需要在此执行:测试之后的teardown--- PASS: TestSplit (0.00s)--- PASS: TestSplit/simple (0.00s)--- PASS: TestSplit/wrong_sep (0.00s)--- PASS: TestSplit/more_sep (0.00s)--- PASS: TestSplit/leading_sep (0.00s)PASSok test/testCode 0.009s
8、示例函数
示例函数格式
被go test特殊对待的第三种函数就是示例函数,它们的函数名以Example为前缀。它们既没有参数也没有返回值。标准格式如下:
func ExampleName() {// ...}
示例函数示例
下面的代码是我们为Split函数编写的一个示例函数:
func ExampleSplit() {fmt.Println(split.Split("a:b:c", ":"))fmt.Println(split.Split("沙河有沙又有河", "沙"))// Output:// [a b c]// [ 河有 又有河]}
为你的代码编写示例代码有如下三个用处:
示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
示例函数只要包含了
// Output:也是可以通过go test运行的可执行测试。split $ go test -run ExamplePASSok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s```</li><li><p>示例函数提供了可以直接运行的示例代码,可以直接在<code>golang.org</code>的<code>godoc</code>文档服务器上使用<code>Go Playground</code>运行示例代码。下图为<code>strings.ToUpper</code>函数在Playground的示例函数效果。<img src="http://www.chenyoude.com/go/example.png?x-oss-process=style/watermark" alt="example.png"/></p></li></ol># 二十、练习题<ol><li>编写一个回文检测函数,并为其编写单元测试和基准测试,根据测试的结果逐步对其进行优化。(回文:一个字符串正序和逆序一样,如“Madam,I’mAdam”、“油灯少灯油”等。)</li></ol>
9、goland生成单元测试和基准测试
单元测试
方式1 :使用goland 开速生成测试用例
1. 在想要测试的方法下面点击鼠标右键 在弹出框选择Generate (快捷键 Alt+Insert)
2. 选择 Test for selecttion
3. goland 会在当前目录下面 生产一个 cache_test.go 的文件并且 ,自动写好测试方法 ,我们需要填充测试数据。
tests 是一个结构体数组包含了我们的测试用例, 每一个测试用例 包含
name(测试用例名称) ,
args(测试方法传入的参数) ,
wantErr (预期结果是否报错)
这个tests 结构根据测试方法不同会有一些差别,但是大体就是 xx_name 测试用例 输入参数是 args,预期输出结果是xxx这样的测试逻辑来的。
func TestSplit(t *testing.T) {type args struct {s stringsep string}tests := []struct {name stringargs argswantResult []string}{// TODO: Add test cases.}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if gotResult := Split(tt.args.s, tt.args.sep); !reflect.DeepEqual(gotResult, tt.wantResult) {t.Errorf("Split() = %v, want %v", gotResult, tt.wantResult)}})}}
