ایجاد یک پوشه برای کدها
برای شروع، یک پوشه (Directory) برای کدهایی که قرار است بنویسید ایجاد کنید.
ترمینال (Command Prompt) را باز کرده و وارد پوشه Home سیستمعامل خود شوید.
در لینوکس یا مک:
$ cd
در ویندوز:
C:\> cd %HOMEPATH%
💡 نکته: در ادامه این آموزش، علامت
$بهعنوان نشانهگر (Prompt) ترمینال استفاده میشود، اما دستورات دادهشده روی ویندوز نیز بهراحتی اجرا میشوند.
حال از طریق ترمینال، پوشهای به نام fuzz برای کدهای خود بسازید و وارد آن شوید:
$ mkdir fuzz
$ cd fuzz
ساخت یک Module برای پروژه
دستور go mod init را اجرا کنید و مسیر Module جدید خود را به آن بدهید:
$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz
💡 نکته: برای کدهای دنیای واقعی و محیط Production، باید مسیر Module اختصاصی خودتان را مشخص کنید. برای اطلاعات بیشتر میتوانید مستندات Managing dependencies را مطالعه کنید.
در مرحله بعد، کد سادهای برای معکوس کردن یک رشته (String) مینویسیم تا در مراحل بعدی آن را Fuzz کنیم.
افزودن کد برای تست
در این مرحله، تابعی برای معکوس کردن یک رشته اضافه خواهید کرد.
نوشتن کد
با استفاده از ویرایشگر متنی خود، فایلی به نام main.go در پوشه fuzz ایجاد کنید.
در بالای فایل main.go، تعریف Package زیر را قرار دهید:
package main
💡 برنامههای مستقل (برخلاف کتابخانهها یا Libraryها) همیشه در
package mainقرار میگیرند.
زیر تعریف Package، تعریف تابع زیر را قرار دهید:
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)
}
این تابع یک رشته را میگیرد، بایت به بایت روی آن پیمایش میکند و در نهایت رشته معکوسشده را برمیگرداند.
💡 نکته: این کد بر اساس تابع stringutil.Reverse در مسیر golang.org/x/example نوشته شده است.
در بالای فایل main.go و زیر تعریف Package، تابع main زیر را اضافه کنید تا یک رشته را مقداردهی اولیه کرده، آن را معکوس کند، خروجی را چاپ نماید و این فرایند را تکرار کند:
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)
}
این تابع چند عملیات Reverse را اجرا کرده و سپس خروجی را در خط فرمان چاپ میکند. این کار برای مشاهده عملکرد کد و در صورت نیاز، اشکالزدایی (Debugging) مفید است.
تابع main از پکیج fmt استفاده میکند، بنابراین باید آن را Import کنید.
خطوط اولیه کد شما باید به این شکل باشد:
package main
import "fmt"
اجرای کد
از طریق خط فرمان و در پوشهای که شامل main.go است، کد را اجرا کنید:
$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"
میبینید که رشته اولیه، نتیجه معکوسسازی آن، و سپس نتیجه معکوسسازی دوباره آن (که معادل رشته اولیه است) در خروجی چاپ شده است.
حال که کد بدون مشکل اجرا میشود، زمان تست کردن آن فرا رسیده است.
افزودن Unit Test
در این مرحله، یک Unit Test ساده برای تابع Reverse خواهید نوشت.
نوشتن کد
با استفاده از ویرایشگر متن، فایلی به نام reverse_test.go در پوشه fuzz بسازید.
کد زیر را درون reverse_test.go قرار دهید:
package main
import (
"testing"
)
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
این تست ساده بررسی میکند که آیا ورودیهای مشخصشده بهدرستی معکوس میشوند یا خیر.
اجرای تست
با استفاده از دستور go test تست واحد را اجرا کنید:
$ go test
PASS
ok example/fuzz 0.013s
در مرحله بعد، این Unit Test را به یک Fuzz Test تبدیل خواهیم کرد.
افزودن Fuzz Test
تستهای واحد (Unit Tests) محدودیتهایی دارند؛ از جمله اینکه تمام ورودیها باید توسط برنامهنویس دستی تعریف شوند. یکی از مزایای Fuzzing این است که خودش ورودیهای مختلفی تولید میکند و میتواند لبههای تاریک کد (Edge Cases) را که در تستهای دستی به آنها فکر نکردهاید، پیدا کند.
در این بخش، Unit Test خود را به یک Fuzz Test تبدیل میکنید تا بتوانید با کار کمتر، ورودیهای بیشتری را بررسی کنید!
💡 نکته: شما میتوانید Unit Testها، Benchmarkها و Fuzz Testها را در یک فایل
*_test.goکنار هم داشته باشید، اما در این آموزش برای سادگی، همان تست قبلی را تبدیل میکنیم.
نوشتن کد
در ویرایشگر خود، کد Unit Test موجود در reverse_test.go را با Fuzz Test زیر جایگزین کنید:
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // استفاده از f.Add برای تامین 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)
}
})
}
فرایند Fuzzing نیز محدودیتهای خود را دارد. در Unit Test میتوانید خروجی مورد انتظار تابع Reverse را پیشبینی کنید؛ برای مثال در تست Reverse("Hello, world") خروجی حتماً "dlrow ,olleH" است. اما هنگام Fuzzing نمیتوان خروجی دقیق را پیشبینی کرد چون کنترلی روی ورودیها ندارید.
با این حال، ویژگیهای خاصی (Properties) در تابع Reverse وجود دارند که میتوان آنها را در Fuzz Test اعتبارسنجی کرد. دو ویژگی بررسیشده در این تست عبارتاند از:
-
معکوس کردن دوبارهی یک رشته، ارزش و مقدار اولیه آن را حفظ میکند.
-
رشته معکوسشده باید یک رشته UTF-8 معتبر باقی بماند.
به تفاوتهای ساختاری بین Unit Test و Fuzz Test دقت کنید:
-
نام تابع بهجای
TestXxxباFuzzXxxشروع میشود و ورودی آن بهجای*testing.Tبرابر*testing.Fاست. -
جایی که انتظار دیدن اجرای
t.Runرا دارید، ازf.Fuzzاستفاده شده است. این تابع یک Fuzz Target میگیرد که پارامترهای آن*testing.Tو انواع دادههایی است که باید Fuzz شوند. ورودیهای Unit Test قبلی نیز با استفاده ازf.Addبهعنوان Seed Corpus اضافه شدهاند.
اطمینان حاصل کنید که پکیج جدید unicode/utf8 وارد (Import) شده باشد:
package main
import (
"testing"
"unicode/utf8"
)
حال که Unit Test به Fuzz Test تبدیل شد، زمان اجرای مجدد تست است.
اجرای کد
تست را بدون فاز Fuzzing فعال اجرا کنید تا مطمئن شوید ورودیهای اولیه (Seed Inputs) پاس میشوند:
$ go test
PASS
ok example/fuzz 0.013s
💡 اگر تستهای دیگری در فایل دارید و میخواهید فقط Fuzz Test اجرا شود، میتوانید از دستور
go test -run=FuzzReverseاستفاده کنید.
برای اینکه بررسی کنید آیا ورودیهای تصادفی باعث خطا میشوند یا خیر، FuzzReverse را همراه با Fuzzing اجرا کنید. این کار با پرچم (Flag) جدید -fuzz در دستور go test انجام میشود:
$ go test -fuzz=Fuzz
پرچم کاربردی دیگر -fuzztime است که زمان اجرای Fuzzing را محدود میکند. بهعناوان مثال، تعیین -fuzztime 10s باعث میشود که اگر خطایی رخ نداد، تست پس از ۱۰ ثانیه متوقف شود.
دستور زیر را اجرا کنید:
$ go test -fuzz=Fuzz
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 38-byte failing input file...
--- FAIL: FuzzReverse (0.01s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"
Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
To re-run:
go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
FAIL
exit status 1
FAIL example/fuzz 0.030s
⚠️ خطا رخ داد! ورودیای که باعث بروز مشکل شده در یک فایل Seed Corpus ذخیره شده است. این فایل در اجراهای بعدی go test (حتی بدون پرچم -fuzz) مجدداً تست خواهد شد. برای مشاهده ورودی ایجادکننده خطا، فایل ذخیرهشده در پوشه testdata/fuzz/FuzzReverse را باز کنید. (ممکن است هش فایل شما متفاوت باشد):
go test fuzz v1
string("泃")
خط اول نسخه Encode را نشان میدهد. خطوط بعدی مقادیر مربوط به ورودی تست هستند. از آنجا که Fuzz Target ما فقط ۱ ورودی میگیرد، تنها ۱ مقدار بعد از نسخه قرار دارد.
دوباره دستور go test را بدون پرچم -fuzz اجرا کنید؛ ورودی معیوب جدید بهصورت خودکار تست میشود:
$ go test
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
reverse_test.go:20: Reverse produced invalid string
FAIL
exit status 1
FAIL example/fuzz 0.016s
از آنجا که تست ما شکست خورده است، زمان اشکالزدایی (Debugging) فرا رسیده است.
رفع خطای Invalid String
در این بخش، خطای رخ داده را عیبیابی و باگ را برطرف میکنیم.
تشخیص خطا
روشهای مختلفی برای Debug این خطا وجود دارد (مانند استفاده از Debugger در VS Code). در این آموزش، اطلاعات مفید را در ترمینال Log میکنیم.
ابتدا مستندات تابع utf8.ValidString را بررسی کنید:
"تابع ValidString بررسی میکند که آیا رشته s کاملاً از Runeهای معتبرِ کدگذاریشده با UTF-8 تشکیل شده است یا خیر."
تابع Reverse فعلی ما رشته را بایت به بایت معکوس میکند و مشکل دقیقاً همینجاست! برای حفظ کاراکترها (Runeها) در یک رشته UTF-8، باید رشته را Rune به Rune معکوس کنیم، نه بایت به بایت.
برای درک بهتر اینکه چرا کاراکتر چینی 泃 باعث ایجاد یک رشته نامعتبر میشود، میتوانیم تعداد Runeها را در رشته معکوسشده بررسی کنیم.
نوشتن کد
در ویرایشگر متن خود، بخش داخل f.Fuzz را با کد زیر جایگزین کنید:
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
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)
}
})
دستور t.Logf در صورت بروز خطا یا هنگام اجرا با پرچم v- جزییات را در ترمینال چاپ میکند.
اجرای کد
تست را اجرا کنید:
$ go test
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL example/fuzz 0.598s
تمام دادههای اولیه ما (Seed Corpus) شامل کاراکترهای ۱ بایتی بودند. اما کاراکترهایی مانند 泃 به چند بایت نیاز دارند. بنابراین، معکوس کردن بایت به بایتِ آنها، کاراکترهای چندبایتی را خراب و نامعتبر میکند.
💡 برای درک بهتر نحوه مدیریت رشتهها در Go، مقاله Strings, bytes, runes and characters in Go را مطالعه کنید.
رفع مشکل
برای اصلاح تابع Reverse، بهجای بایتها، روی Runeها پیمایش میکنیم.
کد تابع Reverse را با مقدار زیر جایگزین کنید:
func Reverse(s string) string {
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)
}
تفاوت کلیدی این است که تابع اکنون به جای بایتها روی Runeها حلقه میزند.
⚠️ توضیح تکمیلی: این یک مثال آموزشی است و ترکیبات پیچیده کاراکتری (Combining Characters) را به طور کامل مدیریت نمیکند.
اجرای مجدد
تست را اجرا کنید:
$ go test
PASS
ok example/fuzz 0.016s
تست پاس شد! حالا دوباره فرایند Fuzzing را اجرا میکنیم تا ببینیم باگ جدیدی کشف میشود یا خیر:
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
fuzz: minimizing 506-byte failing input file...
fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
--- FAIL: FuzzReverse (0.02s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:33: Before: "\x91", after: ""
Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
To re-run:
go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
FAIL
exit status 1
FAIL example/fuzz 0.032s
مشاهده میکنید که رشته پس از ۲ بار معکوس شدن با مقدار اولیه برابر نیست! این بار خود ورودی دادهشده یک Unicode نامعتبر بوده است. چطور چنین چیزی ممکن است؟
زمان اشکالزدایی مجدد است.
رفع خطای معکوسسازی دوگانه (Double Reverse)
در این بخش، خطای دوم را عیبیابی و برطرف میکنیم.
تشخیص خطا
در زبان Go، یک string در واقع یک Slice غیرقابل تغییر (Read-only) از بایتهاست و میتواند حاوی بایتهایی باشد که UTF-8 معتبر نیستند.
ورودی اولیه یک بایت تک '\x91' بوده است. وقتی این رشته به []rune تبدیل میشود، Go آن بایت غیرمجاز را با کاراکتر جایگزین UTF-8 یعنی `` تعویض میکند. طبیعتاً وقتی این کاراکتر جایگزین شده را با بایت اولیه مقایسه میکنیم، برابر نخواهند بود!
نوشتن کد
برای مشاهده دقیقتر، تابع Reverse را بهصورت زیر تغییر دهید تا چاپ لاگ داشته باشیم:
func Reverse(s string) string {
fmt.Printf("input: %q\n", s)
r := []rune(s)
fmt.Printf("runes: %q\n", r)
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)
}
اجرای کد
این بار فقط همان ورودی خاص که باعث شکست شده را با استفاده از پرچم -run اجرا میکنیم:
$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['']
input: ""
runes: ['']
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
reverse_test.go:18: Before: "\x91", after: ""
FAIL
exit status 1
FAIL example/fuzz 0.145s
حالا که متوجه شدیم ورودی اصلی UTF-8 معتبر نیست، تابع را جوری اصلاح میکنیم که ورودیهای غیرمجاز را مدیریت کند.
رفع مشکل
برای حل این مشکل، اگر ورودی تابع Reverse یک UTF-8 معتبر نبود، یک error برمیگردانیم.
کد تابع Reverse را بهصورت زیر بهروزرسانی کنید:
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
}
با توجه به اینکه تابع Reverse اکنون یک Error برمیگرداند، باید تابع main در فایل main.go را هم تغییر دهید:
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
فراموش نکنید پکیجهای errors و unicode/utf8 را در main.go وارد کنید:
import (
"errors"
"fmt"
"unicode/utf8"
)
حالا فایل reverse_test.go را ویرایش کنید تا در صورت بروز Error، از ادامه آن تست صرفنظر (Skip) شود:
func FuzzReverse(f *testing.F) {
testcases := []string {"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // استفاده از f.Add برای تامین 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)
}
})
}
💡 بهجای
returnساده، میتوانید ازt.Skip()نیز برای متوقف کردن اجرای آن ورودی استفاده کنید.
اجرای کد
تست را اجرا کنید:
$ go test
PASS
ok example/fuzz 0.019s
حالا آن را با دستور go test -fuzz=Fuzz به راه بیندازید و پس از چند ثانیه با کلیدهای Ctrl+C متوقف کنید:
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok example/fuzz 228.000s
همچنین میتوانید برای تعیین زمان مشخص (مثلاً ۳۰ ثانیه) از پرچم -fuzztime 30s استفاده کنید:
$ go test -fuzz=Fuzz -fuzztime 30s
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
...
fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
PASS
ok example/fuzz 31.025s
تست Fuzzing با موفقیت پاس شد!
💡 عبارت "new interesting" در خروجی Fuzzing به ورودیهایی اشاره دارد که پوشش کد (Code Coverage) را افزایش دادهاند.
جمعبندی
تبریک میگوییم! شما اولین Fuzz Test خود را در زبان Go با موفقیت پیادهسازی و اجرا کردید.
مرحله بعدی این است که یکی از توابع پروژههای خود را انتخاب کرده و Fuzzing را روی آن امتحان کنید!
کدهای نهایی پروژه
فایل main.go
package main
import (
"errors"
"fmt"
"unicode/utf8"
)
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
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
}
فایل 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) // استفاده از f.Add برای تامین 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)
}
})
}
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات