diff --git a/lib/dump.go b/lib/dump.go new file mode 100644 index 0000000..6520e42 --- /dev/null +++ b/lib/dump.go @@ -0,0 +1,173 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package lib + +import ( + "bytes" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common" +) + +// NewDump returns an evaluation dump that can be used to examine the complete +// set of evaluation states from a CEL program. The program must have been +// constructed with a cel.Env.Program call including the cel.OptTrackState +// evaluation option. The ast and details parameters must be valid for the +// program. +func NewDump(ast *cel.Ast, details *cel.EvalDetails) *Dump { + if ast == nil || details == nil { + return nil + } + return &Dump{ast: ast, det: details} +} + +// Dump is an evaluation dump. +type Dump struct { + ast *cel.Ast + det *cel.EvalDetails +} + +func (d *Dump) String() string { + if d == nil { + return "" + } + var buf strings.Builder + for i, v := range d.NodeValues() { + if i != 0 { + buf.WriteByte('\n') + } + fmt.Fprint(&buf, v) + } + return buf.String() +} + +// NodeValues returns the evaluation results, source location and source +// snippets for the expressions in the dump. The nodes are sorted in +// source order. +func (d *Dump) NodeValues() []NodeValue { + if d == nil { + return nil + } + es := d.det.State() + var values []NodeValue + for _, id := range es.IDs() { + if id == 0 { + continue + } + v, ok := es.Value(id) + if !ok { + continue + } + values = append(values, d.nodeValue(v, id)) + } + sort.Slice(values, func(i, j int) bool { + vi := values[i].loc + vj := values[j].loc + switch { + case vi.Line() < vj.Line(): + return true + case vi.Line() > vj.Line(): + return false + } + switch { + case vi.Column() < vj.Column(): + return true + case vi.Column() > vj.Column(): + return false + default: + // If we are here we have executed more than once + // and have different values, so sort lexically. + // This is not ideal given that values may include + // maps which do not render consistently and so + // we're breaking the sort invariant that comparisons + // will be consistent. For what we are doing this is + // good enough. + return fmt.Sprint(values[i].val) < fmt.Sprint(values[j].val) + } + }) + return values +} + +func (d *Dump) nodeValue(val any, id int64) NodeValue { + v := NodeValue{ + loc: d.ast.NativeRep().SourceInfo().GetStartLocation(id), + src: d.ast.Source(), + val: val, + } + return v +} + +// NodeValue is a CEL expression node value and annotation. +type NodeValue struct { + loc common.Location + src common.Source + val any +} + +func (v NodeValue) MarshalJSON() ([]byte, error) { + type val struct { + Location string `json:"loc"` + Src string `json:"src"` + Val any `json:"val"` + } + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + err := enc.Encode(val{ + Location: v.Loc(), + Src: v.Src(), + Val: v.val, + }) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (v NodeValue) String() string { + return fmt.Sprintf("%s\n%s\n%v\n", v.Loc(), v.Src(), v.Val()) +} + +func (v NodeValue) Val() any { + return v.val +} + +func (v NodeValue) Loc() string { + return fmt.Sprintf("%s:%d:%d", v.src.Description(), v.loc.Line(), v.loc.Column()+1) +} + +func (v NodeValue) Src() string { + snippet, ok := v.src.Snippet(v.loc.Line()) + if !ok { + return "" + } + src := " | " + strings.Replace(snippet, "\t", " ", -1) + ind := "\n | " + strings.Repeat(".", minInt(v.loc.Column(), len(snippet))) + "^" + return src + ind +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/mito.go b/mito.go index d21ee10..77cc65e 100644 --- a/mito.go +++ b/mito.go @@ -74,6 +74,7 @@ func Main() int { logTrace := flag.Bool("log_requests", false, "log request traces to stderr (go1.21+)") maxTraceBody := flag.Int("max_log_body", 1000, "maximum length of body logged in request traces (go1.21+)") fold := flag.Bool("fold", false, "apply constant folding optimisation") + dumpState := flag.String("dump", "", "dump eval state ('always' or 'error')") version := flag.Bool("version", false, "print version and exit") flag.Parse() if *version { @@ -195,8 +196,14 @@ func Main() int { } for n := int(0); *maxExecutions < 0 || n < *maxExecutions; n++ { - res, val, err := eval(string(b), root, input, *fold, libs...) + res, val, dump, err := eval(string(b), root, input, *fold, *dumpState != "", libs...) + if *dumpState == "always" { + fmt.Fprint(os.Stderr, dump) + } if err != nil { + if *dumpState == "error" { + fmt.Fprint(os.Stderr, dump) + } fmt.Fprintln(os.Stderr, err) return 1 } @@ -325,15 +332,20 @@ func debug(tag string, value any) { fmt.Fprintf(os.Stderr, "%s: logging %q: %v\n", level, tag, value) } -func eval(src, root string, input interface{}, fold bool, libs ...cel.EnvOption) (string, any, error) { - prg, ast, err := compile(src, root, fold, libs...) +func eval(src, root string, input interface{}, fold, details bool, libs ...cel.EnvOption) (string, any, *lib.Dump, error) { + prg, ast, err := compile(src, root, fold, details, libs...) if err != nil { - return "", nil, fmt.Errorf("failed program instantiation: %v", err) + return "", nil, nil, fmt.Errorf("failed program instantiation: %v", err) } - return run(prg, ast, false, input) + res, val, det, err := run(prg, ast, false, input) + var dump *lib.Dump + if details { + dump = lib.NewDump(ast, det) + } + return res, val, dump, err } -func compile(src, root string, fold bool, libs ...cel.EnvOption) (cel.Program, *cel.Ast, error) { +func compile(src, root string, fold, details bool, libs ...cel.EnvOption) (cel.Program, *cel.Ast, error) { opts := append([]cel.EnvOption{ cel.Declarations(decls.NewVar(root, decls.Dyn)), }, libs...) @@ -358,40 +370,44 @@ func compile(src, root string, fold bool, libs ...cel.EnvOption) (cel.Program, * } } - prg, err := env.Program(ast) + var progOpts []cel.ProgramOption + if details { + progOpts = []cel.ProgramOption{cel.EvalOptions(cel.OptTrackState)} + } + prg, err := env.Program(ast, progOpts...) if err != nil { return nil, nil, fmt.Errorf("failed program instantiation: %v", err) } return prg, ast, nil } -func run(prg cel.Program, ast *cel.Ast, fast bool, input interface{}) (string, any, error) { +func run(prg cel.Program, ast *cel.Ast, fast bool, input interface{}) (string, any, *cel.EvalDetails, error) { if input == nil { input = interpreter.EmptyActivation() } - out, _, err := prg.Eval(input) + out, det, err := prg.Eval(input) if err != nil { - return "", nil, fmt.Errorf("failed eval: %v", lib.DecoratedError{AST: ast, Err: err}) + return "", nil, det, fmt.Errorf("failed eval: %v", lib.DecoratedError{AST: ast, Err: err}) } v, err := out.ConvertToNative(reflect.TypeOf(&structpb.Value{})) if err != nil { - return "", nil, fmt.Errorf("failed proto conversion: %v", err) + return "", nil, det, fmt.Errorf("failed proto conversion: %v", err) } val := v.(*structpb.Value).AsInterface() if fast { b, err := protojson.MarshalOptions{}.Marshal(v.(proto.Message)) if err != nil { - return "", nil, fmt.Errorf("failed native conversion: %v", err) + return "", nil, det, fmt.Errorf("failed native conversion: %v", err) } - return string(b), val, nil + return string(b), val, det, nil } var buf strings.Builder enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) enc.SetIndent("", "\t") err = enc.Encode(val) - return strings.TrimRight(buf.String(), "\n"), val, err + return strings.TrimRight(buf.String(), "\n"), val, det, err } // rot13 is provided for testing purposes. diff --git a/mito_bench_test.go b/mito_bench_test.go index ff447c5..9770eaa 100644 --- a/mito_bench_test.go +++ b/mito_bench_test.go @@ -46,6 +46,7 @@ var benchmarks = []struct { `"hello world"`, root, fold, + false, ) return prg, ast, nil, err }, @@ -57,6 +58,7 @@ var benchmarks = []struct { `{"greeting":"hello world"}`, root, fold, + false, ) return prg, ast, nil, err }, @@ -68,6 +70,7 @@ var benchmarks = []struct { `{"a":{"b":{"c":{"d":{"e":"f"}}}}}`, root, fold, + false, ) return prg, ast, nil, err }, @@ -79,6 +82,7 @@ var benchmarks = []struct { `{"a":{"b":{"c":{"d":{"e":"f"}}}}}.encode_json()`, root, fold, + false, lib.JSON(nil), ) return prg, ast, nil, err @@ -91,6 +95,7 @@ var benchmarks = []struct { `{"a":{"b":{"c":{"d":{"e":"f"}}}}}.collate("a.b.c.d.e")`, root, fold, + false, lib.Collections(), ) return prg, ast, nil, err @@ -101,7 +106,7 @@ var benchmarks = []struct { { name: "hello_world_state", setup: func(b *testing.B, fold bool) (cel.Program, *cel.Ast, any, error) { - prg, ast, err := compile(root, root, fold) + prg, ast, err := compile(root, root, fold, false) state := map[string]any{root: "hello world"} return prg, ast, state, err }, @@ -113,6 +118,7 @@ var benchmarks = []struct { `{"greeting":state.greeting}`, root, fold, + false, ) state := map[string]any{root: mustParseJSON(`{"greeting": "hello world}"}`)} return prg, ast, state, err @@ -121,7 +127,7 @@ var benchmarks = []struct { { name: "nested_state", setup: func(b *testing.B, fold bool) (cel.Program, *cel.Ast, any, error) { - prg, ast, err := compile(root, root, fold) + prg, ast, err := compile(root, root, fold, false) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} return prg, ast, state, err }, @@ -132,6 +138,7 @@ var benchmarks = []struct { prg, ast, err := compile(`state.encode_json()`, root, fold, + false, lib.JSON(nil), ) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} @@ -151,6 +158,7 @@ var benchmarks = []struct { prg, ast, err := compile(`[state].collate("a.b.c.d.e")`, root, fold, + false, lib.Collections(), ) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} @@ -163,6 +171,7 @@ var benchmarks = []struct { prg, ast, err := compile(`{"state": state}.collate("state.a.b.c.d.e")`, root, fold, + false, lib.Collections(), ) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} @@ -185,6 +194,7 @@ var benchmarks = []struct { fmt.Sprintf(`get(%q).size()`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), ) return prg, ast, nil, err @@ -201,6 +211,7 @@ var benchmarks = []struct { fmt.Sprintf(`string(get(%q).Body)`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), ) return prg, ast, nil, err @@ -217,6 +228,7 @@ var benchmarks = []struct { fmt.Sprintf(`{"greeting":bytes(get(%q).Body).decode_json().greeting}`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) @@ -234,6 +246,7 @@ var benchmarks = []struct { fmt.Sprintf(`bytes(get(%q).Body).decode_json()`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) @@ -251,6 +264,7 @@ var benchmarks = []struct { fmt.Sprintf(`get(%q).Body`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) @@ -270,6 +284,7 @@ var benchmarks = []struct { fmt.Sprintf(`bytes(get(%q).Body).decode_json().encode_json()`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) @@ -287,6 +302,7 @@ var benchmarks = []struct { fmt.Sprintf(`[bytes(get(%q).Body).decode_json()].collate("a.b.c.d.e")`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), lib.Collections(), @@ -305,6 +321,7 @@ var benchmarks = []struct { fmt.Sprintf(`{"body": bytes(get(%q).Body).decode_json()}.collate("body.a.b.c.d.e")`, srv.URL), root, fold, + false, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), lib.Collections(), @@ -332,7 +349,7 @@ func BenchmarkMito(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - v, _, err := run(prg, ast, *fastMarshal, state) + v, _, _, err := run(prg, ast, *fastMarshal, state) if err != nil { b.Fatalf("failed operation: %v", err) } diff --git a/mito_test.go b/mito_test.go index 5484309..d39dd11 100644 --- a/mito_test.go +++ b/mito_test.go @@ -150,7 +150,7 @@ func TestSend(t *testing.T) { got = <-chans["ch"] }() - res, _, err := eval(`42.send_to("ch").close("ch")`, "", nil, fold, send) + res, _, _, err := eval(`42.send_to("ch").close("ch")`, "", nil, fold, false, send) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -257,7 +257,7 @@ func TestVars(t *testing.T) { name = "folded" } t.Run(name, func(t *testing.T) { - got, _, err := eval(src, "", interpreter.EmptyActivation(), fold, vars) + got, _, _, err := eval(src, "", interpreter.EmptyActivation(), fold, false, vars) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -381,7 +381,7 @@ func TestRegaxp(t *testing.T) { name = "folded" } t.Run(name, func(t *testing.T) { - got, _, err := eval(test.src, "", interpreter.EmptyActivation(), fold, lib.Regexp(test.regexps)) + got, _, _, err := eval(test.src, "", interpreter.EmptyActivation(), fold, false, lib.Regexp(test.regexps)) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/testdata/as_dump.txt b/testdata/as_dump.txt new file mode 100644 index 0000000..440ebee --- /dev/null +++ b/testdata/as_dump.txt @@ -0,0 +1,55 @@ +mito -dump always -use collections src.cel +cmp stdout want.txt +cmp stderr want_err.txt + +-- src.cel -- +{"a":1}.as(v, [v, v]) +-- want.txt -- +[ + { + "a": 1 + }, + { + "a": 1 + } +] +-- want_err.txt -- +:1:1 + | {"a":1}.as(v, [v, v]) + | ^ +{a: 1} + +:1:2 + | {"a":1}.as(v, [v, v]) + | .^ +a + +:1:6 + | {"a":1}.as(v, [v, v]) + | .....^ +1 + +:1:11 + | {"a":1}.as(v, [v, v]) + | ..........^ +[] + +:1:11 + | {"a":1}.as(v, [v, v]) + | ..........^ +[{a: 1}, {a: 1}] + +:1:15 + | {"a":1}.as(v, [v, v]) + | ..............^ +[{a: 1}, {a: 1}] + +:1:16 + | {"a":1}.as(v, [v, v]) + | ...............^ +{a: 1} + +:1:19 + | {"a":1}.as(v, [v, v]) + | ..................^ +{a: 1} diff --git a/testdata/optional_types.txt b/testdata/optional_types.txt index 699842b..6cfc516 100644 --- a/testdata/optional_types.txt +++ b/testdata/optional_types.txt @@ -1,6 +1,6 @@ -mito -data state.json src.cel -! stderr . +mito -dump always -data state.json src.cel cmp stdout want.txt +cmp stderr want_err.txt -- state.json -- {"n": 0} @@ -12,3 +12,18 @@ cmp stdout want.txt { "has_x_y_z": false } +-- want_err.txt -- +:1:1 + | { + | ^ +{has_x_y_z: false} + +:2:2 + | "has_x_y_z": has(state.?x.?y.z), + | .^ +has_x_y_z + +:2:18 + | "has_x_y_z": has(state.?x.?y.z), + | .................^ +false diff --git a/testdata/post.txt b/testdata/post.txt index 5612223..e98e9bc 100644 --- a/testdata/post.txt +++ b/testdata/post.txt @@ -1,9 +1,13 @@ +serve hello.text +expand src_var.cel src.cel +cmpenv src.cel src_var.cel + mito -use http,collections src.cel ! stderr . cmp stdout want.txt --- src.cel -- -post("http://www.example.com/", "text/plain", "test").drop([ +-- src_var.cel -- +post("${URL}", "text/plain", "test").drop([ "Header.Accept-Ranges", "Header.Age", "Header.Date", @@ -15,18 +19,22 @@ post("http://www.example.com/", "text/plain", "test").drop([ "Header.Expires", "Header.Server", "TransferEncoding", + "Request.Host", + "Request.URL", ]) +-- hello.text -- +hello -- want.txt -- { - "Body": "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDx0aXRsZT5FeGFtcGxlIERvbWFpbjwvdGl0bGU+CgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiIC8+CiAgICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LXR5cGUiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIgLz4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSIgLz4KICAgIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjBmMGYyOwogICAgICAgIG1hcmdpbjogMDsKICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgIGZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLCBzeXN0ZW0tdWksIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgIk9wZW4gU2FucyIsICJIZWx2ZXRpY2EgTmV1ZSIsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7CiAgICAgICAgCiAgICB9CiAgICBkaXYgewogICAgICAgIHdpZHRoOiA2MDBweDsKICAgICAgICBtYXJnaW46IDVlbSBhdXRvOwogICAgICAgIHBhZGRpbmc6IDJlbTsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZmRmZGZmOwogICAgICAgIGJvcmRlci1yYWRpdXM6IDAuNWVtOwogICAgICAgIGJveC1zaGFkb3c6IDJweCAzcHggN3B4IDJweCByZ2JhKDAsMCwwLDAuMDIpOwogICAgfQogICAgYTpsaW5rLCBhOnZpc2l0ZWQgewogICAgICAgIGNvbG9yOiAjMzg0ODhmOwogICAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgIH0KICAgIEBtZWRpYSAobWF4LXdpZHRoOiA3MDBweCkgewogICAgICAgIGRpdiB7CiAgICAgICAgICAgIG1hcmdpbjogMCBhdXRvOwogICAgICAgICAgICB3aWR0aDogYXV0bzsKICAgICAgICB9CiAgICB9CiAgICA8L3N0eWxlPiAgICAKPC9oZWFkPgoKPGJvZHk+CjxkaXY+CiAgICA8aDE+RXhhbXBsZSBEb21haW48L2gxPgogICAgPHA+VGhpcyBkb21haW4gaXMgZm9yIHVzZSBpbiBpbGx1c3RyYXRpdmUgZXhhbXBsZXMgaW4gZG9jdW1lbnRzLiBZb3UgbWF5IHVzZSB0aGlzCiAgICBkb21haW4gaW4gbGl0ZXJhdHVyZSB3aXRob3V0IHByaW9yIGNvb3JkaW5hdGlvbiBvciBhc2tpbmcgZm9yIHBlcm1pc3Npb24uPC9wPgogICAgPHA+PGEgaHJlZj0iaHR0cHM6Ly93d3cuaWFuYS5vcmcvZG9tYWlucy9leGFtcGxlIj5Nb3JlIGluZm9ybWF0aW9uLi4uPC9hPjwvcD4KPC9kaXY+CjwvYm9keT4KPC9odG1sPgo=", + "Body": "aGVsbG8K", "Close": false, - "ContentLength": 1256, + "ContentLength": 6, "Header": { "Content-Length": [ - "1256" + "6" ], "Content-Type": [ - "text/html; charset=UTF-8" + "text/plain; charset=utf-8" ] }, "Proto": "HTTP/1.1", @@ -40,12 +48,10 @@ post("http://www.example.com/", "text/plain", "test").drop([ "text/plain" ] }, - "Host": "www.example.com", "Method": "POST", "Proto": "HTTP/1.1", "ProtoMajor": 1, - "ProtoMinor": 1, - "URL": "http://www.example.com/" + "ProtoMinor": 1 }, "Status": "200 OK", "StatusCode": 200, diff --git a/testdata/request_do.txt b/testdata/request_do.txt index 38da383..f7a1e6a 100644 --- a/testdata/request_do.txt +++ b/testdata/request_do.txt @@ -1,30 +1,35 @@ +serve hello.text +expand src_var.cel src.cel +cmpenv src.cel src_var.cel + mito -use http,collections src.cel ! stderr . cmp stdout want.txt --- src.cel -- -post_request("http://www.example.com/", "text/plain", "request data").do_request().drop([ +-- src_var.cel -- +post_request("${URL}", "text/plain", "request data").do_request().drop([ "Header.Cache-Control", "Header.Date", "Header.Etag", "Header.Expires", "Header.Last-Modified", "Header.Server", + "Request.Host", + "Request.URL", ]) +-- hello.text -- +hello -- want.txt -- { - "Body": "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDx0aXRsZT5FeGFtcGxlIERvbWFpbjwvdGl0bGU+CgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiIC8+CiAgICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LXR5cGUiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIgLz4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSIgLz4KICAgIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjBmMGYyOwogICAgICAgIG1hcmdpbjogMDsKICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgIGZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLCBzeXN0ZW0tdWksIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgIk9wZW4gU2FucyIsICJIZWx2ZXRpY2EgTmV1ZSIsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7CiAgICAgICAgCiAgICB9CiAgICBkaXYgewogICAgICAgIHdpZHRoOiA2MDBweDsKICAgICAgICBtYXJnaW46IDVlbSBhdXRvOwogICAgICAgIHBhZGRpbmc6IDJlbTsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZmRmZGZmOwogICAgICAgIGJvcmRlci1yYWRpdXM6IDAuNWVtOwogICAgICAgIGJveC1zaGFkb3c6IDJweCAzcHggN3B4IDJweCByZ2JhKDAsMCwwLDAuMDIpOwogICAgfQogICAgYTpsaW5rLCBhOnZpc2l0ZWQgewogICAgICAgIGNvbG9yOiAjMzg0ODhmOwogICAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgIH0KICAgIEBtZWRpYSAobWF4LXdpZHRoOiA3MDBweCkgewogICAgICAgIGRpdiB7CiAgICAgICAgICAgIG1hcmdpbjogMCBhdXRvOwogICAgICAgICAgICB3aWR0aDogYXV0bzsKICAgICAgICB9CiAgICB9CiAgICA8L3N0eWxlPiAgICAKPC9oZWFkPgoKPGJvZHk+CjxkaXY+CiAgICA8aDE+RXhhbXBsZSBEb21haW48L2gxPgogICAgPHA+VGhpcyBkb21haW4gaXMgZm9yIHVzZSBpbiBpbGx1c3RyYXRpdmUgZXhhbXBsZXMgaW4gZG9jdW1lbnRzLiBZb3UgbWF5IHVzZSB0aGlzCiAgICBkb21haW4gaW4gbGl0ZXJhdHVyZSB3aXRob3V0IHByaW9yIGNvb3JkaW5hdGlvbiBvciBhc2tpbmcgZm9yIHBlcm1pc3Npb24uPC9wPgogICAgPHA+PGEgaHJlZj0iaHR0cHM6Ly93d3cuaWFuYS5vcmcvZG9tYWlucy9leGFtcGxlIj5Nb3JlIGluZm9ybWF0aW9uLi4uPC9hPjwvcD4KPC9kaXY+CjwvYm9keT4KPC9odG1sPgo=", + "Body": "aGVsbG8K", "Close": false, - "ContentLength": 1256, + "ContentLength": 6, "Header": { - "Accept-Ranges": [ - "bytes" - ], "Content-Length": [ - "1256" + "6" ], "Content-Type": [ - "text/html; charset=UTF-8" + "text/plain; charset=utf-8" ] }, "Proto": "HTTP/1.1", @@ -38,12 +43,10 @@ post_request("http://www.example.com/", "text/plain", "request data").do_request "text/plain" ] }, - "Host": "www.example.com", "Method": "POST", "Proto": "HTTP/1.1", "ProtoMajor": 1, - "ProtoMinor": 1, - "URL": "http://www.example.com/" + "ProtoMinor": 1 }, "Status": "200 OK", "StatusCode": 200,