Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package fortio for openSUSE:Factory checked in at 2022-04-05 19:55:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/fortio (Old) and /work/SRC/openSUSE:Factory/.fortio.new.1900 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "fortio" Tue Apr 5 19:55:35 2022 rev:6 rq:966955 version:1.25.0 Changes: -------- --- /work/SRC/openSUSE:Factory/fortio/fortio.changes 2022-03-28 17:01:52.369076287 +0200 +++ /work/SRC/openSUSE:Factory/.fortio.new.1900/fortio.changes 2022-04-05 19:56:02.141844080 +0200 @@ -1,0 +2,6 @@ +Tue Apr 05 06:52:14 UTC 2022 - ka...@b1-systems.de + +- Update to version 1.25.0: + * support for gzip with new gzip=true echo param (or gzip=10 for 10%) (#541) + +------------------------------------------------------------------- Old: ---- fortio-1.24.0.tar.gz New: ---- fortio-1.25.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ fortio.spec ++++++ --- /var/tmp/diff_new_pack.fZghCs/_old 2022-04-05 19:56:05.557805996 +0200 +++ /var/tmp/diff_new_pack.fZghCs/_new 2022-04-05 19:56:05.561805950 +0200 @@ -19,7 +19,7 @@ %define __arch_install_post export NO_BRP_STRIP_DEBUG=true Name: fortio -Version: 1.24.0 +Version: 1.25.0 Release: 0 Summary: Load testing library, command line tool, advanced echo server and web UI License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.fZghCs/_old 2022-04-05 19:56:05.589805639 +0200 +++ /var/tmp/diff_new_pack.fZghCs/_new 2022-04-05 19:56:05.589805639 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/fortio/fortio</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.24.0</param> + <param name="revision">v1.25.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> @@ -16,7 +16,7 @@ <param name="compression">gz</param> </service> <service name="go_modules" mode="disabled"> - <param name="archive">fortio-1.24.0.tar.gz</param> + <param name="archive">fortio-1.25.0.tar.gz</param> </service> </services> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.fZghCs/_old 2022-04-05 19:56:05.605805461 +0200 +++ /var/tmp/diff_new_pack.fZghCs/_new 2022-04-05 19:56:05.609805415 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/fortio/fortio</param> - <param name="changesrevision">fb00657cd46406b6660f5fc1fa4894d6c426d295</param></service></servicedata> + <param name="changesrevision">3eed83884d1264b2faa10dc3fc2b0517ae2eae8d</param></service></servicedata> (No newline at EOF) ++++++ fortio-1.24.0.tar.gz -> fortio-1.25.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fortio-1.24.0/README.md new/fortio-1.25.0/README.md --- old/fortio-1.24.0/README.md 2022-03-28 00:47:13.000000000 +0200 +++ new/fortio-1.25.0/README.md 2022-04-04 19:09:59.000000000 +0200 @@ -46,13 +46,13 @@ Or download one of the binary distributions, from the [releases](https://github.com/fortio/fortio/releases) assets page or for instance: ```shell -curl -L https://github.com/fortio/fortio/releases/download/v1.24.0/fortio-linux_x64-1.24.0.tgz \ +curl -L https://github.com/fortio/fortio/releases/download/v1.25.0/fortio-linux_x64-1.25.0.tgz \ | sudo tar -C / -xvzpf - # or the debian package -wget https://github.com/fortio/fortio/releases/download/v1.24.0/fortio_1.24.0_amd64.deb -dpkg -i fortio_1.24.0_amd64.deb +wget https://github.com/fortio/fortio/releases/download/v1.25.0/fortio_1.25.0_amd64.deb +dpkg -i fortio_1.25.0_amd64.deb # or the rpm -rpm -i https://github.com/fortio/fortio/releases/download/v1.24.0/fortio-1.24.0-1.x86_64.rpm +rpm -i https://github.com/fortio/fortio/releases/download/v1.25.0/fortio-1.25.0-1.x86_64.rpm ``` On a MacOS you can also install Fortio using [Homebrew](https://brew.sh/): @@ -61,7 +61,7 @@ brew install fortio ``` -On Windows, download https://github.com/fortio/fortio/releases/download/v1.24.0/fortio_win_1.24.0.zip and extract `fortio.exe` to any location, then using the Windows Command Prompt: +On Windows, download https://github.com/fortio/fortio/releases/download/v1.25.0/fortio_win_1.25.0.zip and extract `fortio.exe` to any location, then using the Windows Command Prompt: ``` fortio.exe server ``` @@ -106,7 +106,7 @@ <details> <!-- use release/updateFlags.sh to update this section --> <pre> -???????????? 1.24.0 usage: +???????????? 1.25.0 usage: where command is one of: load (load testing), server (starts ui, http-echo, redirect, proxies, tcp-echo and grpc ping servers), tcp-echo (only the tcp-echo server), report (report only UI server), redirect (only the redirect server), @@ -330,6 +330,7 @@ | size | size of the payload to reply instead of echoing input. Also works as probabilities list. `size=1024:10,512:5` 10% of response will be 1k and 5% will be 512 bytes payload and the rest defaults to echoing back. | | close | close the socket after answering e.g `close=true` to close after all requests or `close=5.3` to close after approximately 5.3% of requests| | header | header(s) to add to the reply e.g. `&header=Foo:Bar&header=X:Y` | +| gzip | If `Accept-Encoding: gzip` is passed in headers by the caller/client; and `gzip=true` is in the query args, all response will be gzipped; or if `gzip=42.7` is passed, approximately 42.7% will| You can set a default value for all these by passing `-echo-server-default-params` to the server command line, for instance: `fortio server -echo-server-default-params="delay=0.5s:50,1s:40&status=418"` will make the server respond with http 418 and a delay of either 0.5s half of the time, 1s 40% and no delay in 10% of the calls; unless any `?` query args is passed by the client. Note that the quotes (") are for the shell to escape the ampersand (&) but should not be put in a yaml nor the dynamicflag url for instance. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fortio-1.24.0/fhttp/http_client.go new/fortio-1.25.0/fhttp/http_client.go --- old/fortio-1.24.0/fhttp/http_client.go 2022-03-28 00:47:13.000000000 +0200 +++ new/fortio-1.25.0/fhttp/http_client.go 2022-04-04 19:09:59.000000000 +0200 @@ -650,13 +650,13 @@ if c.https { socket, err = tls.Dial(c.dest.Network(), c.dest.String(), c.tlsConfig) if err != nil { - log.Errf("Unable to TLS connect to %v : %v", c.dest, err) + log.Errf("[%d] Unable to TLS connect to %v : %v", c.id, c.dest, err) return nil } } else { socket, err = net.Dial(c.dest.Network(), c.dest.String()) if err != nil { - log.Errf("Unable to connect to %v : %v", c.dest, err) + log.Errf("[%d] Unable to connect to %v : %v", c.id, c.dest, err) return nil } } @@ -701,28 +701,28 @@ if err != nil || conErr != nil { if reuse { // it's ok for the (idle) socket to die once, auto reconnect: - log.Infof("Closing dead socket %v (%v)", conn, err) + log.Infof("[%d] Closing dead socket %v (%v)", c.id, c.dest, err) conn.Close() c.errorCount++ return c.Fetch() // recurse once } - log.Errf("Unable to write to %v %v : %v", conn, c.dest, err) + log.Errf("[%d] Unable to write to %v : %v", c.id, c.dest, err) return c.returnRes() } if n != len(c.req) { - log.Errf("Short write to %v %v : %d instead of %d", conn, c.dest, n, len(c.req)) + log.Errf("[%d] Short write to %v : %d instead of %d", c.id, c.dest, n, len(c.req)) return c.returnRes() } if !c.keepAlive && c.halfClose { // nolint: nestif tcpConn, ok := conn.(*net.TCPConn) if ok { if err = tcpConn.CloseWrite(); err != nil { - log.Errf("Unable to close write to %v %v : %v", conn, c.dest, err) + log.Errf("[%d] Unable to close write to %v : %v", c.id, c.dest, err) return c.returnRes() } // else: log.Debugf("Half closed ok after sending request %v %v", conn, c.dest) } else { - log.Warnf("[%d] Unable to close write non tcp connection %v", c.id, conn) + log.Warnf("[%d] Unable to close write non tcp connection %v", c.id, c.dest) } } // Read the response: @@ -760,11 +760,11 @@ if err != nil { if reusedSocket && c.size == 0 { // Ok for reused socket to be dead once (close by server) - log.Infof("Closing dead socket %v (err %v at first read)", conn, err) + log.Infof("[%d] Closing dead socket %v (err %v at first read)", c.id, c.dest, err) c.errorCount++ err = conn.Close() // close the previous one if err != nil { - log.Warnf("[%d] Error closing dead socket %v: %v", c.id, conn, err) + log.Warnf("[%d] Error closing dead socket %v: %v", c.id, c.dest, err) } c.code = RetryOnce // special "retry once" code return @@ -773,7 +773,7 @@ // handled below as possibly normal end of stream after we read something break } - log.Errf("Read error %v %v %d : %v", conn, c.dest, c.size, err) + log.Errf("[%d] Read error for %v %d : %v", c.id, c.dest, c.size, err) c.code = SocketError break } @@ -870,7 +870,7 @@ } if checkConnectionClosedHeader { if found, _ := FoldFind(c.buffer[:c.headerLen], connectionCloseHeader); found { - log.Infof("Server wants to close connection, no keep-alive!") + log.Infof("[%d] Server wants to close connection, no keep-alive!", c.id) keepAlive = false max = len(c.buffer) // reset to read as much as available } @@ -923,7 +923,7 @@ c.socket = conn // keep the open socket } else { if err := conn.Close(); err != nil { - log.Errf("Close error %v %v %d : %v", conn, c.dest, c.size, err) + log.Errf("[%d] Close error %v %d : %v", c.id, c.dest, c.size, err) } else { log.Debugf("Closed ok %v from %v after reading %d bytes", conn, c.dest, c.size) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fortio-1.24.0/fhttp/http_server.go new/fortio-1.25.0/fhttp/http_server.go --- old/fortio-1.24.0/fhttp/http_server.go 2022-03-28 00:47:13.000000000 +0200 +++ new/fortio-1.25.0/fhttp/http_server.go 2022-04-04 19:09:59.000000000 +0200 @@ -102,6 +102,12 @@ log.Debugf("Adding Connection:close / will close socket") w.Header().Set("Connection", "close") } + gzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && generateGzip(r.FormValue("gzip")) + if gzip { + gwz := NewGzipHTTPResponseWriter(w) + defer gwz.Close() + w = gwz + } // process header(s) args, must be before size to compose properly for _, hdr := range r.Form["header"] { log.LogVf("Adding requested header %s", hdr) @@ -355,7 +361,7 @@ return nil, nil // error already logged } if debugPath != "" { - mux.HandleFunc(debugPath, DebugHandler) + mux.Handle(debugPath, Gzip(http.HandlerFunc(DebugHandler))) mux.HandleFunc(EchoDebugPath(debugPath), EchoHandler) // Fix #524 } mux.HandleFunc("/", EchoHandler) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fortio-1.24.0/fhttp/http_test.go new/fortio-1.25.0/fhttp/http_test.go --- old/fortio-1.24.0/fhttp/http_test.go 2022-03-28 00:47:13.000000000 +0200 +++ new/fortio-1.25.0/fhttp/http_test.go 2022-04-04 19:09:59.000000000 +0200 @@ -515,6 +515,28 @@ } } +func TestGenerateGzip(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + // not numbers + {"true", true}, + {"false", false}, + {"x", true}, + // Numbers + {"0", false}, + {"0.0", false}, + {"99.9999999", true}, // well, in theory this should fail once in a blue moon + {"100", true}, + } + for _, tst := range tests { + if actual := generateGzip(tst.input); actual != tst.expected { + t.Errorf("Got %v, expected %v for generateGzip(%q)", actual, tst.expected, tst.input) + } + } +} + func TestPayloadWithEchoBack(t *testing.T) { tests := []struct { payload []byte diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fortio-1.24.0/fhttp/http_utils.go new/fortio-1.25.0/fhttp/http_utils.go --- old/fortio-1.24.0/fhttp/http_utils.go 2022-03-28 00:47:13.000000000 +0200 +++ new/fortio-1.25.0/fhttp/http_utils.go 2022-04-04 19:09:59.000000000 +0200 @@ -15,6 +15,7 @@ package fhttp // import "fortio.org/fortio/fhttp" import ( + "compress/gzip" "crypto/tls" "crypto/x509" "encoding/base64" @@ -444,33 +445,109 @@ return 0 } -// generateClose from string, format: close=true for 100% close -// close=true:10 or close=10 for 10% socket close. -func generateClose(closeStr string) bool { - if closeStr == "" || closeStr == "false" { +// generateSingleProbability takes a string value and a name and returns a boolean. +// false if the value is missing or "false". +// true if the value is "true" or doesn't parse as a floating point number. +// otherwise if the value is a floating point number X; use it as a percentage +// and roll a dice to be true X% of the time. +func generateSingleProbability(value string, name string) bool { + if value == "" || value == "false" { return false } - if closeStr == "true" { // avoid throwing error for pre 1.22 syntax + if value == "true" { // avoid throwing error for pre 1.22 syntax return true } - p, err := strconv.ParseFloat(closeStr, 32) + p, err := strconv.ParseFloat(value, 32) if err != nil { - log.Debugf("error %v parsing close=%q treating as true", err, closeStr) + log.Debugf("error %v parsing %s=%q treating as true", err, name, value) return true } res := 100. * rand.Float32() // nolint: gosec // we want fast not crypto - log.Debugf("close=%f rolled %f", p, res) + log.Debugf("%s=%f rolled %f", name, p, res) if res <= float32(p) { return true } return false } +// generateClose from string, format: close=true for 100% close +// close=true:10 or close=10 for 10% socket close. +func generateClose(closeStr string) bool { + return generateSingleProbability(closeStr, "close") +} + +// generateGzip from string, format: gzip=true or gzip=100 for 100% gzip +// gzip=42.3 for 42.3% gzip result (if Accept-Encoding is gzip). +func generateGzip(gzipStr string) bool { + return generateSingleProbability(gzipStr, "gzip") +} + // RoundDuration rounds to 10th of second. func RoundDuration(d time.Duration) time.Duration { return d.Round(100 * time.Millisecond) } +// Inspired by https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7 (thanks!) +// (with fixes/adaptation) + +var gzPool = sync.Pool{ + New: func() interface{} { + log.LogVf("Pool new gzip") + w := gzip.NewWriter(ioutil.Discard) + return w + }, +} + +// GzipResponseWriter wraps the response and gzips the content. +type GzipResponseWriter struct { + io.Writer + http.ResponseWriter + gz *gzip.Writer +} + +// WriteHeader intercepts the actual to remove any Content-Length that may have been added before compression. +func (w *GzipResponseWriter) WriteHeader(status int) { + w.ResponseWriter.Header().Del("Content-Length") + w.ResponseWriter.WriteHeader(status) +} + +// Write sends the Write() to the gzip Writer. +func (w *GzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +// Close must be called in defer inside the handler using this. +func (w *GzipResponseWriter) Close() error { + err := w.gz.Close() + gzPool.Put(w.gz) + w.gz = nil // just in case there is a bug, will be NPE instead of race + return err +} + +// NewGzipHTTPResponseWriter returns a wrapper for gzip'ing the response. +func NewGzipHTTPResponseWriter(w http.ResponseWriter) *GzipResponseWriter { + log.LogVf("Doing gzip compression") + w.Header().Set("Content-Encoding", "gzip") + gz := gzPool.Get().(*gzip.Writer) + gz.Reset(w) + return &GzipResponseWriter{ResponseWriter: w, Writer: gz, gz: gz} +} + +// Gzip wraps a handler for automatic gzip. +func Gzip(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // in our case we only wrap if we decided (gzip=x % rolled true) to gzip and so we already checked headers + // but leaving the check so this can be reused in generic code. + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + gzw := NewGzipHTTPResponseWriter(w) + defer gzw.Close() + next.ServeHTTP(gzw, r) + }) +} + // -- formerly in uihandler: // HTMLEscapeWriter is an io.Writer escaping the output for safe html inclusion. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fortio-1.24.0/fhttp/httprunner_test.go new/fortio-1.25.0/fhttp/httprunner_test.go --- old/fortio-1.24.0/fhttp/httprunner_test.go 2022-03-28 00:47:13.000000000 +0200 +++ new/fortio-1.25.0/fhttp/httprunner_test.go 2022-04-04 19:09:59.000000000 +0200 @@ -16,6 +16,8 @@ package fhttp import ( + "bytes" + "compress/gzip" "fmt" "net/http" "runtime" @@ -227,6 +229,24 @@ log.Infof("Got expected error from mismatch/bad server: %v", err) } +func gUnzipData(t *testing.T, data []byte) (resData []byte) { + b := bytes.NewBuffer(data) + + r, err := gzip.NewReader(b) + if err != nil { + t.Errorf("gunzip NewReader: %v", err) + return + } + var resB bytes.Buffer + _, err = resB.ReadFrom(r) + if err != nil { + t.Errorf("gunzip ReadFrom: %v", err) + return + } + resData = resB.Bytes() + return +} + // need to be the last test as it installs Serve() which would make // the error test for / url above fail: @@ -259,11 +279,68 @@ c2, _ := NewClient(o2) code2, data2, header := c2.Fetch() if code2 != http.StatusOK { - t.Errorf("Unexpected non 200 ret code for debug url %s : %d", url, code) + t.Errorf("Unexpected non 200 ret code for debug echo url %s : %d", url2, code2) } if string(data2[header:]) != "abcd" { t.Errorf("Unexpected that %s isn't an echo server, got %q", url2, string(data2)) } + // Accept gzip but no actual gzip=true + o2.AddAndValidateExtraHeader("Accept-Encoding: gzip") + c3, _ := NewClient(o2) + code3, data3, header := c3.Fetch() + if code3 != http.StatusOK { + t.Errorf("Unexpected non 200 ret code for debug echo url %s : %d", url2, code3) + } + if string(data3[header:]) != "abcd" { + t.Errorf("Unexpected that %s with Accept-Encoding: gzip but no gzip true isn't a plain echo server, got %q", url2, string(data3)) + } + url4 := url2 + "?gzip=true" + o4 := NewHTTPOptions(url4) + expected4 := "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + o4.Payload = []byte(expected4) + o4.AddAndValidateExtraHeader("Accept-Encoding: gzip") + c4, _ := NewClient(o4) + code4, data4, header := c4.Fetch() + if code4 != http.StatusOK { + t.Errorf("Unexpected non 200 ret code for debug echo gziped url %s : %d", url4, code4) + } + if string(data4[header:]) == expected4 { + t.Errorf("Unexpected that %s with Accept-Encoding: gzip and ?gzip=true is a plain echo server, got %q", url4, string(data4)) + } + if len(data4)-header >= len(expected4) { + t.Errorf("Unexpected that %s with Accept-Encoding: gzip and ?gzip=true returns bigger payload %d (not compressed), got %q", + url4, len(data4)-header, string(data4)) + } + data4unzip := gUnzipData(t, data4[header:]) + if string(data4unzip) != expected4 { + t.Errorf("Unexpected that %s with Accept-Encoding: gzip and ?gzip=true doesn't gunzip to echo, got %q", url4, string(data4)) + } + url5 := url4 + "&size=400" + o5 := NewHTTPOptions(url5) + c5, _ := NewClient(o5) + code5, data5, header := c5.Fetch() + if code5 != http.StatusOK { + t.Errorf("Unexpected non 200 ret code for debug echo gziped url %s : %d", url5, code5) + } + expected6 := data5[header:] // when we actually compress we should get same as this after gunzip + if len(data5)-header != 400 { + t.Errorf("Unexpected that %s without Accept-Encoding: gzip and ?gzip=true&size=400 should return 400 bytes, got %d", + url5, len(data5)-header) + } + o5.AddAndValidateExtraHeader("Accept-Encoding: gzip") + c6, _ := NewClient(o5) + code6, data6, header := c6.Fetch() + if code6 != http.StatusOK { + t.Errorf("Unexpected non 200 ret code for debug echo gziped url %s : %d", url5, code6) + } + data6unzip := gUnzipData(t, data6[header:]) + if !bytes.Equal(data6unzip, expected6) { + t.Errorf("Unexpected that %s with Accept-Encoding: gzip and ?gzip=true doesn't gunzip to echo, got %q", url5, string(data6)) + } + if len(data6)-header <= 400 { + t.Errorf("Unexpected that %s with Accept-Encoding: gzip and ?gzip=true and random payload compresses to lower than 400: %d", + url5, len(data6)-header) + } } func TestAbortOn(t *testing.T) { ++++++ vendor.tar.gz ++++++