Ez a bejegyzés elmagyarázza ennek a kísérletnek a motivációját, bemutatja, hogyan kell használni a synctest csomagot, és megvitatja annak lehetséges jövőjét. A 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 csomagot, amely támogatja a kísérleti kód tesztelését. testing/synctest szinkronizálás és tesztelés A Go 1.24-ben a 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 kódmal kell összeállítania. testing/synctest GOEXPERIMENT=synctest 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 függvény megszervezi, hogy egy függvényt a kontextus törlése után saját goroutine-jében hívjanak. context.AfterFunc 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. az Könnyedén ellenőrizhetjük, hogy a függvényt még nem hívták , de hogyan ellenőrizhetjük, hogy ? yet nem hívják és még nem fog 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 rendszereken. CI Tovább A csomag csak két funkciót tartalmaz: és . Kezdőlap » Kódok » Kódok 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 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. testing/synctest szinkronizálás és tesztelés Run Wait Forrás 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 Run buboréknak nevezünk. arra vár, hogy minden goroutine az aktuális goroutine buborékában blokkoljon egy másik goroutine-t a buborékban. Wait Kezdőlap » Kódok » Kódok Forrás buborék Vegyük át a fenti tesztünket a csomag használatával. testing/synctest 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") }) } Kezdőlap » Kezdőlap » Kezdőlap » Kód Kezdőlap » Kódok » Kódok Korábban egy csatornát kellett használnunk, hogy elkerüljük a tesztgoroutine és a goroutine közötti adatversenyt, de a funkció most biztosítja ezt a szinkronizációt. Kezdőlap » Kódok » Kódok Kezdőlap » Kódok » Kódok Kezdőlap » Kódok » KódokKezdőlap » Kódok » KódokKezdőlap » Kódok » Kódok 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 hívásba csomagoltuk, és a funkció hívása előtt hívunk. synctest.Run synctest.Wait szinkronizálás és futtatás A 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. Wait Ez a teszt mostantól gyors és megbízható. AfterFunc Wait A munkavégzés után A versenyérzékelő megérti a hívásokat, és ez a teszt akkor halad át, ha a segítségével fut. Ha eltávolítjuk a második hívást, a versenyérzékelő helyesen jelent egy adatversenyt a tesztben. Wait -race Wait Tesztelé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 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. time idő A csomag megkönnyíti az időt használó kód tesztelését. testing/synctest szinkronizálás és tesztelés A buborékban a által indított golyók hamis órát használnak. A buborékban a csomag funkciói a hamis órán működnek. Run time Forrás idő A demonstrációhoz írjunk egy tesztet a funkcióra. létrehoz egy kontextus gyermekeit, amely egy adott időzítés után lejár. context.WithTimeout WithTimeout ÖsszefüggésKezdőlap » Kódok » Kódok context.WithTimeout funkció 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 -ba csomagoljuk, és minden egyes -hívás után hívjuk a -t, hogy várjuk a kontextuscsomag időzítőit a futás befejezéséig. Kezdőlap » Kezdőlap » Kezdőlap » Kód synctest.Run time.Sleep synctest.Wait szinkronizálás és futtatás idő.alvás Blocking és a buborék Ha van egy kiemelkedő 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 pánikba esik. szinkronizálás Megtekintés szinkronizálása Hasonlóképpen . 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. A egyik legfontosabb koncepciója az, hogy a buborék tartósan blokkolva lesz testing/synctest szinkronizálás és tesztelés tartósan blokkolva Amikor egy buborék tartósan blokkolva van: Ha van egy kilépő hívás, akkor visszatér. Wait 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 pánik. Run Wait Run 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 a küld vagy fogad egy nil csatornán a küldés vagy fogadás blokkolva van egy ugyanabban a buborékban létrehozott csatornán egy kiválasztott mondat, amelyben minden eset tartósan blokkolódik idő.Alvás idő.alvás sync.Cond.Wait sync.Cond.Wait sync.WaitGroup.Wait sync.WaitGroup.Wait Mutasítások A műveletek nem tartósan blokkolnak. sync.Mutex sync.Mutex 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 megfontolásából. testing/synctest 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 funkció egy pár -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 net.Conn net.Pipe és net.Conn Bubble élettartam A 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. Run 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 csomagot használva egy hálózati program tesztelésére. testing/synctest 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 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. testing/synctest 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. ExpectContinueTimeout 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, 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 -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 strings.Builder beállítása Kezdőlap » Kezdőlap » Kezdőlap » KódKezdőlap » Kódok » Kódok A hívás megvárja, hogy mindannyian kilépjenek, mielőtt visszatérnének. Ha kipróbálod a lehetőséget, kérjük, jelentsd a pozitív vagy negatív tapasztalataidat a Ha elfelejtjük a hívást, a versenyérzékelő helyesen panaszkodik egy adatversenyre, de a hívással ez biztonságos. synctest.Wait Wait 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. synctest.Run 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 a Go 1.24-ben, mint egy 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. testing/synctest experimentális 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ó beállítással. GOEXPERIMENT=synctest GYIK=szinkronizálás testing/synctest címen. » HR Főszerepben Damien Neil go.dev/issue/67434 szinkronizálás és tesztelés go.dev/issue/67434 Hitelek: Damien Neil Hitelek : Photo by on Gabriel Gusmao Unsplash Fotó: on Gabriel Gusmao Unsplash Gabriel Gusmao Unsplash Ez a cikk elérhető a egy CC BY 4.0 DEED licenc alatt. The Go Blog Ez a cikk elérhető a egy CC BY 4.0 DEED licenc alatt. The Go Blog A Go Blog A Go Blog