Go1.18의 generics 및 fuzing에 대한 접촉 최소화

개요


트위터 봤어요.
https://go.dev/blog/tutorials-go1.18
다가오는 발매를 위해 최소한의 접촉을 해봤습니다.

사전 준비


왜냐하면 Go1.18은 베타 버전이거든요.
그걸 설치해야 돼요.
$ go install golang.org/dl/go1.18beta1@latest
go: downloading golang.org/dl v0.0.0-20220106205509-1eec60721618
$
$ go1.18beta1 download
Downloaded   0.0% (    16384 / 143162528 bytes) ...
Downloaded 100.0% (143162528 / 143162528 bytes)
Unpacking /Users/su/sdk/go1.18beta1/go1.18beta1.darwin-amd64.tar.gz ...
Success. You may now run 'go1.18beta1'
$
$ go1.18beta1 version
go version go1.18beta1 darwin/amd64
$
$ alias go=go1.18beta1
$ go version
go version go1.18beta1 darwin/amd64

generics


여기 다 써있어요.
https://go.dev/doc/tutorial/generics

이루어지다


만약 지금까지int64float64가 각각 함수를 정의해야 한다면
generics로 K와 V를 정의합시다.단지 이와 같다
잘 써라!!
main.go
package main

import "fmt"

type Number interface {
	int64 | float64
}

func main() {
	// Initialize a map for the integer values
	ints := map[string]int64{
		"first": 34,
		"second": 12,
	}

	// Initialize a map for the float values
	floats := map[string]float64{
		"first": 35.98,
		"second": 26.99,
	}

	fmt.Printf("Generic Sums with Constraint: %v and %v\n",
		SumNumbers[string, int64](ints),
		SumNumbers[string, float64](floats))
}

// SumNumbers sums the values of map m. Its supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}
실행 결과
$ go run main.go
Generic Sums with Constraint: 46 and 62.97

설치(시스템 생략)


generics에서 설명하는 함수를 호출할 때
형식을 명확하게 정의하지 않아도 이해할 수 있다
main.go
	fmt.Printf("Generic Sums with Constraint: %v and %v\n",
		SumNumbers(ints),
		SumNumbers(floats),
	)

fuzzing


유닛 시험이 아니라 fuzz 시험인 것 같아요.
https://go.dev/doc/tutorial/fuzz
unit 시험에서 자신이 준비한 데이터를 통과하다
실행된 결과가 준비된 기대치와 일치하는지 검증해야 하지만
fuzz 테스트에서 데이터를 미리 준비하지 않아도
각양각색의 데이터를 깊이 파고들면 자신이 완전히 배려할 수 없는 범위에서도 테스트를 할 수 있다!

차리다

go test노는 거니까 적당히 module 이름으로.
$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz

설치(문제 있음)


이 문자열을 역순으로 하는 함수를 테스트합니다
"abc"를 입력하면 "cba"출력이 돌아오는 느낌.
main.go
package main

import (
	"fmt"
)

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev := Reverse(input)
    doubleRev := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q\n", rev)
    fmt.Printf("reversed again: %q\n", doubleRev)
}

func Reverse(s string) string {
    b := []byte(s)
    for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}

Fuzz 테스트


단일 테스트의 작법과 매우 비슷하다
단일 테스트에서 TestXxx를 쓸 때 FuzzXxx를 사용합니다go mod init가 아니라 *testing.T 미묘하게 다르게 사용했어요.
  • 두 번 반복한 다음 원래 문자열
  • 로 돌아갑니다.
  • utf8에 적합한 문자열인지 확인
  • 에 대한 2 개의 인증
    reverse_test.go
    package main
    
    import (
        "testing"
        "unicode/utf8"
    )
    
    func FuzzReverse(f *testing.F) {
        testcases := []string{"Hello, world", " ", "!12345"}
        for _, tc := range testcases {
            f.Add(tc)  // Use f.Add to provide a seed corpus
        }
        f.Fuzz(func(t *testing.T, orig string) {
            rev := Reverse(orig)
            doubleRev := Reverse(rev)
            if orig != doubleRev {
                t.Errorf("Before: %q, after: %q", orig, doubleRev)
            }
            if utf8.ValidString(orig) && !utf8.ValidString(rev) {
                t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
            }
        })
    }
    
    실제로 실행해 보도록 하겠습니다.
    $ go test -v
    === RUN   FuzzReverse
    === RUN   FuzzReverse/seed#0
    === RUN   FuzzReverse/seed#1
    === RUN   FuzzReverse/seed#2
    --- PASS: FuzzReverse (0.00s)
        --- PASS: FuzzReverse/seed#0 (0.00s)
        --- PASS: FuzzReverse/seed#1 (0.00s)
        --- PASS: FuzzReverse/seed#2 (0.00s)
    PASS
    ok  	example/fuzz	0.178s
    
    4개의 데이터가 PASS입니다!
    ...이게 유닛 테스트랑 뭐가 달라요?그렇게 생각하지만.
    이런 느낌으로 *testing.Fflag 실행을 하면...
    $ go test -fuzz=Fuzz -v
    === FUZZ  FuzzReverse
    fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
    fuzz: minimizing 32-byte failing input file
    fuzz: elapsed: 0s, minimizing
    --- FAIL: FuzzReverse (0.22s)
        --- FAIL: FuzzReverse (0.00s)
            reverse_test.go:20: Reverse produced invalid UTF-8 string "\xae\xcd"
    
        Failing input written to testdata/fuzz/FuzzReverse/9b504024244a9afd5840f8f96d7a0cfd880663007b6495535a0e3c28bdea6241
        To re-run:
        go test -run=FuzzReverse/9b504024244a9afd5840f8f96d7a0cfd880663007b6495535a0e3c28bdea6241
    FAIL
    exit status 1
    FAIL	example/fuzz	0.561s
    
    UTF-8에 적합한 문자열이 아니라는 욕!!-fuzzflag 없이 -fuzz자신만의 테스트 데이터만 테스트
    flag를 켜면 테스트는 자동으로 각종 데이터로 실행됩니다
    그리고 이 Fuzz 시험은 영원히.
    따라서 f.Add(tc) 몇 초 동안 매개 변수를 지정해야 합니다.

    설치 수정


    몇 번 뛰고 또 욕을 많이 먹었다는 느낌.
    main.go
    package main
    
    import (
    	"errors"
    	"fmt"
    	"log"
    	"unicode/utf8"
    )
    
    func main() {
    	input := "The quick brown fox jumped over the lazy dog"
    	rev, err := Reverse(input)
    	if err != nil {
    		log.Fatal(err)
    	}
    	doubleRev, err := Reverse(rev)
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("original: %q\n", input)
    	fmt.Printf("reversed: %q\n", rev)
    	fmt.Printf("reversed again: %q\n", doubleRev)
    }
    
    func Reverse(s string) (string, error) {
    	if !utf8.ValidString(s) {
    		return s, errors.New("input is not valid UTF-8")
    	}
    	r := []rune(s)
    	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
    		r[i], r[j] = r[j], r[i]
    	}
    	return string(r), nil
    }
    

    Fuzz 테스트 다시 수행

    -fuzztime 5s 오류가 있으면 nil 또는 Reverse로 돌아갑니다.
    reverse_test.go
    package main
    
    import (
    	"testing"
    	"unicode/utf8"
    )
    
    func FuzzReverse(f *testing.F) {
    	testcases := []string{"Hello, world", " ", "!12345"}
    	for _, tc := range testcases {
    		f.Add(tc) // Use f.Add to provide a seed corpus
    	}
    	f.Fuzz(func(t *testing.T, orig string) {
    		rev, err1 := Reverse(orig)
    		if err1 != nil {
    			return
    		}
    		doubleRev, err2 := Reverse(rev)
    		if err2 != nil {
    			return
    		}
    		if orig != doubleRev {
    			t.Errorf("Before: %q, after: %q", orig, doubleRev)
    		}
    		if utf8.ValidString(orig) && !utf8.ValidString(rev) {
    			t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    		}
    	})
    }
    
    이렇게 또 집행을 하면...
    $ go test -fuzz=Fuzz -fuzztime 5s -v
    === FUZZ  FuzzReverse
    fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 37/37 completed, now fuzzing with 8 workers
    fuzz: elapsed: 3s, execs: 576016 (191999/sec), new interesting: 0 (total: 35)
    fuzz: elapsed: 5s, execs: 975339 (190528/sec), new interesting: 0 (total: 35)
    --- PASS: FuzzReverse (5.10s)
    PASS
    ok  	example/fuzz	5.204s
    
    ok 성공!!

    총결산


    둘 다 속공하는 녀석이야.
    정식 발매 후 바로 사용하세요!

    좋은 웹페이지 즐겨찾기