A Go egyik aláírási funkciója a párhuzamos támogatás.Goroutins és csatornák egyszerű és hatékony primitívek a párhuzamos programok írásához.
Az egyidejű programok tesztelése azonban nehéz és hibás lehet.
A Go 1.24-ben bemutatunk egy új, kísérleti testing/synctest
csomagot, amely támogatja a kísérleti kód tesztelését.
szinkronizálás és tesztelés
A Go 1.24-ben a testing/synctest
csomag kísérleti jellegű, és nem tartozik a Go kompatibilitási ígéret hatálya alá. Alapértelmezés szerint nem látható. Ahhoz, hogy használni tudja, a kódot a környezetében beállított GOEXPERIMENT=synctest
kódmal kell összeállítania.
szinkronizálás és tesztelés
GYIK=szinkronizálás
A párhuzamos programok tesztelése nehéz
Kezdjük egy egyszerű példával.
A context.AfterFunc
függvény megszervezi, hogy egy függvényt a kontextus törlése után saját goroutine-jében hívjanak.context.AfterMűködés
A munkavégzés után
funkció TestAfterFunc(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) calledCh := make(chan struct{}) // zárva, amikor az AfterFunc context.AfterFunc(ctx, func() { close(calledCh) }) // TODO: Azt állítja, hogy az AfterFunc nem hívott. cancel() // TODO: Azt állítja, hogy az AfterFunc hívott. }
func TestAfterFunc(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) calledCh := make(chan struct{}) // zárva, amikor az AfterFunc context.AfterFunc(ctx, func() { close(calledCh) }) // TODO: Azt állítja, hogy az AfterFunc nem hívott. cancel() // TODO: Azt állítja, hogy az AfterFunc hívott. }
Ebben a tesztben két feltételt szeretnénk ellenőrizni: A függvényt a kontextus törlése előtt nem hívják, és a függvényt a kontextus törlése után hívják.
A az
Egy közös megközelítés az, hogy várjunk egy kis időt, mielőtt arra a következtetésre jutnánk, hogy egy esemény nem fog megtörténni.
// funcCalled jelzi, hogy a funkció hívásra került-e. funcCalled := func() bool { select { case <-calledCh: return true case <-time.After(10 * time.Millisecond): return false } } ha funcCalled() { t.Fatalf("AfterFunc funkció hívása a kontextus törlése előtt") } cancel() ha!funcCalled() { t.Fatalf("AfterFunc funkció nem hívott a kontextus törlése után") }
// funcCalled jelzi, hogy a funkciót hívták-e. funcCalled := func() bool { select { case <-calledCh: return true case <-time.After(10 * time.Millisecond): return false } } ha funcCalled() { t.Fatalf("AfterFunc funkció hívása a kontextus törlése előtt") } cancel() ha!funcCalled() { t.Fatalf("AfterFunc funkció nem hívott a kontextus törlése után") }
Ez a teszt lassú: 10 milliszekundum nem sok idő, de sok tesztnél felhalmozódik.
Ez a teszt is homályos: 10 milliszekundum hosszú idő egy gyors számítógépen, de nem szokatlan, hogy több másodperces szüneteket látunk a megosztott és túlterhelt CI rendszereken.
Tovább
A tesztet lassabbá tehetjük, a tesztet lassabbá tehetjük, de nem tudjuk gyorsabbá és megbízhatóbbá tenni.
A tesztelési/szinktatási csomag bevezetése
A testing/synctest
csomag megoldja ezt a problémát. lehetővé teszi számunkra, hogy ezt a tesztet egyszerűen, gyorsan és megbízhatóan írjuk le, anélkül, hogy bármilyen változást végeznénk a tesztelt kódban.
szinkronizálás és tesztelés
Run
és Wait
.Forrás
Kezdőlap » Kódok » Kódok
Run
egy funkciót hív egy új goroutine-ben. Ez a goroutine és az általa elindított bármely goroutine egy elszigetelt környezetben létezik, amelyet buboréknak nevezünk. Wait
arra vár, hogy minden goroutine az aktuális goroutine buborékában blokkoljon egy másik goroutine-t a buborékban.
Forrás
buborékKezdőlap » Kódok » Kódok
Vegyük át a fenti tesztünket a testing/synctest
csomag használatával.
szinkronizálás és tesztelés
funkció TestAfterFunc(t *testing.T) { synctest.Run(func() { ctx, cancel := context.WithCancel(context.Background()) funcCalled := false context.AfterFunc(ctx, func() { funcCalled = true }) synctest.Wait() ha funcCalled { t.Fatalf("AfterFunc funkció hívott, mielőtt a kontextus törlődik") } cancel() synctest.Wait() ha!funcCalled { t.Fatalf("AfterFunc funkció nem hívott, miután a kontextus törlődik") }) } funkció TestAfterFunc(t *testing.T) { synctest.Run(func() { ctx, cancel := context.WithCancel(context.Background()) funcCalled := false context.AfterFunc(ctx, func() { funcCalled = true }) synctest.Wait() ha funcCalled { t.Fatalf("AfterFunc funkció hívott, mielőtt a kontextus törlődik") } cancel() synctest.Wait() ha!funcCalled { t.Fatalf("AfterFunc funkció nem hívott, miután a kontextus törlődik") } } } }
Ez majdnem azonos az eredeti tesztünkkel, de a tesztet egy synctest.Run
hívásba csomagoltuk, és a funkció hívása előtt synctest.Wait
hívunk.
szinkronizálás és futtatás
Kezdőlap » Kezdőlap » Kezdőlap » Kód
A Wait
függvény arra vár, hogy a hívó buborékában minden goroutine blokkoljon. Amikor visszatér, tudjuk, hogy a kontextuscsomag vagy felhívta a függvényt, vagy nem hívja meg, amíg nem teszünk további lépéseket.
Kezdőlap » Kódok » Kódok
Ez a teszt mostantól gyors és megbízható.
Korábban egy csatornát kellett használnunk, hogy elkerüljük a tesztgoroutine és a AfterFunc
goroutine közötti adatversenyt, de a Wait
funkció most biztosítja ezt a szinkronizációt.
Kezdőlap » Kódok » KódokA munkavégzés után
Kezdőlap » Kódok » Kódok
A versenyérzékelő megérti a Wait
hívásokat, és ez a teszt akkor halad át, ha a -race
segítségével fut. Ha eltávolítjuk a második Wait
hívást, a versenyérzékelő helyesen jelent egy adatversenyt a tesztben.
Kezdőlap » Kódok » KódokKezdőlap » Kódok » KódokKezdőlap » Kódok » KódokTesztelési idő
A versengő kód gyakran foglalkozik az idővel.
Az idővel működő kód tesztelése nehéz lehet.A valós idejű tesztek használata lassú és homályos teszteket eredményez, amint azt fentebb láttuk.A hamis idő használata megköveteli a time
csomagfunkciók elkerülését, és a tesztelés alatt álló kód megtervezését, hogy egy opcionális hamis órával működjön.
idő
A testing/synctest
csomag megkönnyíti az időt használó kód tesztelését.
szinkronizálás és tesztelés
A buborékban a Run
által indított golyók hamis órát használnak. A buborékban a time
csomag funkciói a hamis órán működnek.Forrás
idő
A demonstrációhoz írjunk egy tesztet a context.WithTimeout
funkcióra. WithTimeout
létrehoz egy kontextus gyermekeit, amely egy adott időzítés után lejár.
context.WithTimeout
ÖsszefüggésKezdőlap » Kódok » Kódokfunkció TestWithTimeout(t *testing.T) { synctest.Run(func() { const timeout = 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Wait csak kevesebb, mint a timeout. time.Sleep(timeout - time.Nanosecond) synctest.Wait() ha err : ctx.Err(); err!= nil { t.Fatalf("before timeout, ctx.Err() = %v; want nil", err) } // Wait a többi úton, amíg a timeout time.Sleeptime.Sleeptime(Nanosecond) synctest.Wait() ha err : ct= ctx.Err(),func TestWithTimeout(t *testing.T) { synctest.Run(func() { const timeout = 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Wait csak kevesebb, mint a timeout. time.Sleep(timeout - time.Nanosecond) synctest.Wait() ha err := ctx.Err(); err!= nil { t.Fatalf("before timeout, ctx.Err() = %v; want nil", err) } // Wait a többi út, amíg a timeout time.Sleeptime(Nanosecond) synctest.Wait() ha : err= ctx.Err(); err!= context.DeadlineExAz egyetlen különbség az, hogy a tesztfunkciót a synctest.Run
-ba csomagoljuk, és minden egyes time.Sleep
-hívás után hívjuk a synctest.Wait
-t, hogy várjuk a kontextuscsomag időzítőit a futás befejezéséig.
szinkronizálás és futtatás
Kezdőlap » Kezdőlap » Kezdőlap » Kód idő.alvás
Blocking és a buborék
A testing/synctest
egyik legfontosabb koncepciója az, hogy a buborék tartósan blokkolva lesz
. Ez akkor történik meg, amikor a buborék minden goroutine blokkolva van, és csak egy másik goroutine képes feloldani a buborék blokkolását.
szinkronizálás és tesztelés
tartósan blokkolva
Amikor egy buborék tartósan blokkolva van:
- Ha van egy kilépő
Wait
hívás, akkor visszatér. - Ellenkező esetben az idő előrehalad a következő alkalommal, amely feloldhatja a goroutine, ha van.
- Ellenkező esetben a buborék zárva van, és
Run
pánik.
Wait
hívás, akkor visszatér.Kezdőlap » Kódok » KódokEllenkező esetben az idő előrehalad a következő alkalomra, amely feloldhatja a goroutint, ha van.Ellenkező esetben a buborék le van zárva, és a Run
pánikba esik.Forrás
Egy buborék nem tartósan blokkolva, ha bármilyen goroutin blokkolva van, de felébredhet valamilyen esemény a buborékon kívülről.
Az olyan műveletek teljes listája, amelyek tartósan blokkolják a goroutint:
- a küldés vagy fogadás egy nil csatornán
- a küldés vagy fogadás blokkolva egy ugyanabban a buborékban létrehozott csatornán
- egy olyan kijelentés, amelyben minden eset tartósan blokkolja
time.Sleep
sync.Cond.Wait
sync.WaitGroup.Wait
idő.Alvás
idő.alvás
sync.Cond.Wait
sync.Cond.Wait
szinkronizálássync.WaitGroup.Wait
sync.WaitGroup.Wait
MegtekintésMutasítások
A sync.Mutex
műveletek nem tartósan blokkolnak.
sync.Mutex
szinkronizálása
Gyakori, hogy a funkciók globális mutex-t szereznek meg. Például a tükröződő csomag számos funkciója egy mutex által védett globális gyorsítótárat használ. Ha egy goroutin egy synctest buborékban blokkol, miközben egy goroutin által a buborékon kívül tartott mutexet szerez, akkor nem tartósan blokkolva van – blokkolva van, de a buborékon kívülről egy goroutin feloldja a blokkolását.
Mivel a mutexeket általában nem tartják hosszú ideig, egyszerűen kizárjuk őket a testing/synctest
megfontolásából.
szinkronizálás és tesztelés
Hálózati csatornák
A buborékban létrehozott csatornák eltérően viselkednek, mint a külsőben létrehozott csatornák.
A csatorna-műveletek csak akkor blokkolnak tartósan, ha a csatorna buborékos (a buborékban keletkezik).
Ezek a szabályok biztosítják, hogy a goroutin csak akkor kerül tartósan blokkolásra, ha a buborékban lévő goroutinokkal kommunikál.
I / O
A külső I/O műveletek, például a hálózati kapcsolaton keresztüli olvasás nem blokkolják tartósan.
A hálózati olvasmányokat a buborékon kívülről, esetleg más folyamatokból származó írások is feloldhatják. Még akkor is, ha a hálózati kapcsolat egyetlen írója ugyanabban a buborékban van, a futási idő nem tudja megkülönböztetni a kapcsolatot, amely több adat megérkezését várja, és azt, amelyben a kernel adatokat kapott, és folyamatban van.
A hálózati kiszolgáló vagy kliens tesztelése a synctest segítségével általában hamis hálózati végrehajtást igényel. Például a net.Pipe
funkció egy pár net.Conn
-ot hoz létre, amelyek a memóriában lévő hálózati kapcsolatot használják, és a synctestben használhatók.
net.Pipe és
net.Conn
HasonlóképpenBubble élettartam
A Run
funkció elindítja a goroutint egy új buborékban. Visszatér, amikor a buborékban minden goroutint elhagyott. pánikba esik, ha a buborék tartósan blokkolva van, és az idő előrehaladásával nem lehet feloldani.
Forrás
A követelmény, hogy minden goroutine a buborék kijárat előtt a futás visszatér azt jelenti, hogy a tesztek kell óvatosan tisztítsa meg a háttér goroutine befejezése előtt.
Hálózati kód tesztelése
Vessünk egy másik példát, ezúttal a testing/synctest
csomagot használva egy hálózati program tesztelésére.szinkronizálás és tesztelés
hálózat / HTTP
A kérést küldő HTTP kliens tartalmazhat egy „Várj: 100-folytatás” fejlécet, amely megmondja a kiszolgálónak, hogy a kliensnek további adata van küldeni.A kiszolgáló 100 Folytatás információs válaszsal válaszolhat a kérés többi részének megkeresésére, vagy egy másik állapotgal, amely megmondja a kliensnek, hogy a tartalom nem szükséges. Például egy nagy fájlt feltöltő kliens használhatja ezt a funkciót annak megerősítésére, hogy a kiszolgáló hajlandó elfogadni a fájlt a küldés előtt.
A tesztünk megerősíti, hogy a „Várj: 100-folyton” fejléc elküldésekor a HTTP-ügyfél nem küldi el a kérés tartalmát, mielőtt a kiszolgáló kéri, és hogy a tartalom 100 Folytatott válasz után küldi el.
Gyakran egy kommunikáló kliens és szerver tesztelése loopback hálózati kapcsolatot használhat.A testing/synctest
használatakor azonban általában egy hamis hálózati kapcsolatot szeretnénk használni, amely lehetővé teszi számunkra, hogy észleljük, mikor minden goroutine blokkolva van a hálózaton.szinkronizálás és tesztelés
HTTP Szállítmányozás
net.Pipe és
func Test(t *testing.T) { synctest.Run(func() { srvConn, cliConn := net.Pipe() defer srvConn.Close() defer cliConn.Close() tr := &http.Transport{ DialContext: func(ctx context.Context, hálózat, címsor) (net.Conn, hiba) { return cliConn, nil }, // egy nem-zéró időtávolság beállítása lehetővé teszi a "Expect: 100-continue" kezelést. // Mivel a következő teszt nem alszik, // soha nem találkozunk ezzel az időtávolsággal, // még akkor is, ha a teszt hosszú ideig tart egy lassú gépen. ExpectContinueTimeoutfunc Test(t *testing.T) { synctest.Run(func() { srvConn, cliConn := net.Pipe() defer srvConn.Close() defer cliConn.Close() tr := &http.Transport{ DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { return cliConn, nil }, // A nem-zéró időzítés beállítása lehetővé teszi a "Expect: 100-continue" kezelést. // Mivel a következő teszt nem alszik, // soha nem találkozunk ezzel az időzítéssel, // még akkor is, ha a teszt hosszú ideig tart egy lassú gépen.
Ezen a szállításon kérést küldünk a „Remélem: 100 folyamatos” fejléc-készlettel.A kérést új goroutine-ben küldjük el, mivel a teszt végéig nem fejeződik be.
test := "kérj testet" menjen func() { req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body)) req.Header.Set("Expect", "100-folytatás") resp, err := tr.RoundTrip(req) ha err!= nil { t.Errorf("RoundTrip: unexpected error %v", err) } else resp {.Body.Close() }()
body := "request body" go func() { req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body)) req.Header.Set("Expect", "100-continue") resp, err := tr.RoundTrip(req) ha err!= nil { t.Errorf("RoundTrip: unexpected error %v", err) } egyéb { resp.Body.Close() }()
Elolvashatjuk az ügyfél által küldött kérési fejléceket.
req, err := http.ReadRequest(bufio.NewReader(srvConn)) ha err!= nil { t.Fatalf("ReadRequest: %v", err) }
req, err := http.ReadRequest(bufio.NewReader(srvConn)) if err!= nil { t.Fatalf("ReadRequest: %v", err) }
Most a teszt középpontjába kerülünk. szeretnénk kijelenteni, hogy az ügyfél még nem küldi el a kérelmet.
Elkezdünk egy új goroutint másolni a szerverre küldött testet egy strings.Builder
-ba, várjuk, hogy a buborékban lévő összes goroutint blokkoljuk, és ellenőrizzük, hogy még nem olvastunk semmit a testből.
strings.Builder beállítása
Ha elfelejtjük a synctest.Wait
hívást, a versenyérzékelő helyesen panaszkodik egy adatversenyre, de a Wait
hívással ez biztonságos.
Kezdőlap » Kezdőlap » Kezdőlap » KódKezdőlap » Kódok » Kódok var gotBody strings.Builder go io.Copy(&gotBody, req.Body) synctest.Wait() if got := gotBody.String(); got!= "" { t.Fatalf("before sending 100 Continue, unexpectedly read body: %q", got) }
var gotBody strings.Builder go io.Copy(&gotBody, req.Body) synctest.Wait() ha kapott := gotBody.String(); kapott!= "" { t.Fatalf("előtte küldött 100 Folytatás, váratlanul olvasott test: %q", kapott) }
Írunk egy „100 Folytatás” válaszot az ügyfélnek, és ellenőrizzük, hogy most elküldi a kérelmet.
srvConn.Write([]byte("HTTP/1.1 100 Continue\r\r\n\n")) synctest.Wait() ha kapott := gotBody.String(); kapott!= test { t.Fatalf("szállítás után 100 Folytatás, olvassa el a test %q, akarja %q", kapott, test) }
srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\n\n")) synctest.Wait() ha kapott := gotBody.String(); kapott!= test { t.Fatalf("szállítás után 100 Folytatás, olvassa el test %q, akar %q", kapott, test) }
És végül a „200 OK” válasz küldésével fejezzük be a kérést.
A synctest.Run
hívás megvárja, hogy mindannyian kilépjenek, mielőtt visszatérnének.
szinkronizálás és futtatás
srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n") }) }
srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n") }) }
Ez a teszt könnyen kiterjeszthető más viselkedések tesztelésére, például annak ellenőrzésére, hogy a kérelmet nem küldi el, ha a szerver nem kéri, vagy hogy küldi el, ha a szerver nem válaszol időtartam alatt.
A kísérlet állapota
Mi bevezetjük a testing/synctest
a Go 1.24-ben, mint egy experimentális csomagot.A visszajelzésektől és a tapasztalattól függően kiadhatjuk módosításokkal vagy anélkül, folytathatjuk a kísérletet, vagy eltávolíthatjuk a Go jövőbeli verziójában.
szinkronizálás és tesztelés
kísérletezés
A csomag alapértelmezés szerint nem látható. Ahhoz, hogy használhassa, összeállítsa a kódot a környezetében található GOEXPERIMENT=synctest
beállítással.
GYIK=szinkronizálás
Ha kipróbálod a testing/synctest
lehetőséget, kérjük, jelentsd a pozitív vagy negatív tapasztalataidat a go.dev/issue/67434 címen.szinkronizálás és tesztelés
go.dev/issue/67434» HRHitelek: Damien Neil
Hitelek :Főszerepben Damien Neil
Photo by Gabriel Gusmao on Unsplash
Fotó: Gabriel Gusmao on UnsplashGabriel GusmaoUnsplash
Ez a cikk elérhető a
Ez a cikk elérhető a