Merge Mock - testing tool for the Ethereum Merge

Overview

MergeMock

Experimental debug tooling, mocking the execution engine and consensus node for testing.

work in progress

Quick Start

To get started, build mergemock and download the genesis.json.

$ go build . mergemock
$ wget https://gist.githubusercontent.com/lightclient/799c727e826483a2804fc5013d0d3e3d/raw/2e8824fa8d9d9b040f351b86b75c66868fb9b115/genesis.json
$ ./mergemock engine
$ ./mergemock consensus --slot-time=4s

Usage

engine

$ mergemock engine --help

Run a mock Execution Engine.

  --past-genesis              Time past genesis (can be negative for pre-genesis) (default: 0s) (type: duration)
  --slot-time                 Time per slot (default: 0s) (type: duration)
  --slots-per-epoch           Slots per epoch (default: 0) (type: uint64)
  --datadir                   Directory to store execution chain data (empty for in-memory data) (type: string)
  --genesis                   Genesis execution-config file (type: string)
  --listen-addr               Address to bind RPC HTTP server to (default: 127.0.0.1:8550) (type: string)
  --ws-addr                   Address to serve /ws endpoint on for websocket JSON-RPC (default: 127.0.0.1:8551) (type: string)
  --cors                      List of allowable origins (CORS http header) (default: *) (type: stringSlice)

# log
Change logger configuration

  --log.level                 Log level: trace, debug, info, warn/warning, error, fatal, panic. Capitals are accepted too. (default: info) (type: string)
  --log.color                 Color the log output. Defaults to true if terminal is detected. (default: true) (type: bool)
  --log.format                Format the log output. Supported formats: 'text', 'json' (default: text) (type: string)
  --log.timestamps            Timestamp format in logging. Empty disables timestamps. (default: 2006-01-02T15:04:05Z07:00) (type: string)

# timeout
Configure timeouts of the HTTP servers

  --timeout.read              Timeout for body reads. None if 0. (default: 30s) (type: duration)
  --timeout.read-header       Timeout for header reads. None if 0. (default: 10s) (type: duration)
  --timeout.write             Timeout for writes. None if 0. (default: 30s) (type: duration)
  --timeout.idle              Timeout to disconnect idle client connections. None if 0. (default: 5m0s) (type: duration)

consensus

$ mergemock consensus --help

Run a mock Consensus client.

  --past-genesis              Time past genesis (can be negative for pre-genesis) (default: 0s) (type: duration)
  --slot-time                 Time per slot (default: 12s) (type: duration)
  --slots-per-epoch           Slots per epoch (default: 32) (type: uint64)
  --engine                    Address of Engine JSON-RPC endpoint to use (default: http://127.0.0.1:8550) (type: string)
  --datadir                   Directory to store execution chain data (empty for in-memory data) (type: string)
  --genesis                   Genesis execution-config file (default: genesis.json) (type: string)
  --rng                       seed the RNG with an integer number (default: 1234) (type: RNG)

# freq
Modify frequencies of certain behavior

  --freq.gap                  How often an execution block is missing (default: 0.05) (type: float64)
  --freq.proposal             How often the engine gets to propose a block (default: 0.2) (type: float64)
  --freq.ignore               How often the payload produced by the engine does not become canonical (default: 0.1) (type: float64)
  --freq.finality             How often an epoch succeeds to finalize (default: 0.1) (type: float64)

# log
Change logger configuration

  --log.level                 Log level: trace, debug, info, warn/warning, error, fatal, panic. Capitals are accepted too. (default: info) (type: string)
  --log.color                 Color the log output. Defaults to true if terminal is detected. (default: true) (type: bool)
  --log.format                Format the log output. Supported formats: 'text', 'json' (default: text) (type: string)
  --log.timestamps            Timestamp format in logging. Empty disables timestamps. (default: 2006-01-02T15:04:05Z07:00) (type: string)

Behaviour

Execution Engine Mock

Consensus clients that desire to test their use of the Engine API will benefit from a "mocked" execution engine. Mocking the Engine API from the execution side is relatively straightforward since the transition is an opaque process to the consensus client.

engine_preparePayload

  • Creates an ExecutionPayload object with the request's parameters.
    • receiptRoot is a random hash.
    • extraData is a random value.
    • gasLimit is a random value between 29,000,000 and 31,000,000.
    • gasUsed is a random value between 21,000 * len(txs) and gasLimit.
    • baseFee is a random value greater than 7.
    • transactions is an array of between 0 and 100 random transactions.
  • A unique identifier that internally maps to the payload is returned.

engine_getPayload

  • Returns the ExecutionPayload associated with the PayloadId

engine_executePayload

  • Returns the status of the execution.

engine_consensusValidated

  • Essentially a no-op.

engine_forkchoiceUpdated

  • Essentially a no-op.
  • TODO: what should the mock do if this is called to finalize a block that is already the ancestor of a finalized block?

Ideas for CLI args to improve mocking

  • A CLI flag to simulate "syncing", so all RPC methods return values as if the client were currently syncing.
  • A CLI arg of "known" header hashes (or just headers?) to text error code 4: Unknown header.
  • A CLI arg of "valid" / "invalid" header hashes.
  • A CLI arg of percentile values to determine the probability of certain errors happening.

Consensus Client Mock

Mocking the consensus client is more involved. As the driver of the execution engine, it needs to be more intelligent with its requests.

The general idea is to simulate slots and epochs of configurable intervals and lengths. With a basic slot cycle, the rest of the behaviour can be defined to occur based on some probability factor.

The most difficult method to model will likely be engine_executePayload since it can't just use random transactions. To actually provide a "real" chain to the execution engine, it will probably be best if mergemock also maintains a copy of the chain and applies transfer txs to it to get a new transition.

Rough Order of Operation

  1. Slot begins.
  2. Determine if mergemock will propose this slot.
  3. If yes, call engine_preparePayload. 3a. After a short period of time, call engine_getPayload.
  4. If no, generate an ExecutionPayload and call engine_executePayload. 4a. Call engine_consensusValidated.
  5. engine_forkchoiceUpdated is called.
    • Question: Is this called at this point to update the head to the parent block (if proposing) and to the newly executed block if not?

Probability Configuration

  • proposal -- the probability that the consensus client executes the engine_preparePayload, engine_getPayload flow.
  • missed_proposal -- the probability the consensus client's proposal is not built upon. This basically means the engine_forkchoiceUpdated is called with a new head hash that orphans the proposal.
  • invalid_payload -- the probability that an ExecutionPayload is valid. The consensus client can construct "invalid" payloads in numerous ways; one of the easier ones may be to just set the state_root to 0x000..000.

Other Configuration

  • slot_time -- the time between slots.
  • epoch_length -- number of slots in an epoch.
  • finalization_offset -- the number of epochs to wait before announcing to the execution engine a finalized block.

License

MIT, see LICENSE file.

Comments
  • added ci + make lint, fixed staticcheck complaints

    added ci + make lint, fixed staticcheck complaints

    This PR adds a Makefile with make lint and make test, and fixes all complaints by gofmt, go vet and [staticcheck](https://staticcheck.io/). Adds a Github CI workflow.

    Linting does the following commands:

    gofmt -d ./
    go vet ./...
    staticcheck ./...
    

    Not sure this is something you want, but thought I might as well offer it as contribution :)

    opened by metachris 7
  • Bugs computing hash tree root in `BuilderBid`

    Bugs computing hash tree root in `BuilderBid`

    When I use mergemock to compute the hash tree root of a BuilderBid, I get some errors:

    1. incorrect hash tree root of the U256Str type (e.g. in the BaseFeePerGas field in the ExecutionPayloadHeader type)
    2. incorrect hash tree root of the ExtraData field of the ExecutionPayloadHeader type

    (1) is incorrect bc it stores a big int value as bytes in big-endian order -- this is convenient for the textual serialization but there already exists an SSZ implementation for [32]byte (the underlying type) which takes the bytes as-is... after trying some things to fix locally, I'd suggest we just use the Uint256View from ztyp: https://github.com/protolambda/ztyp/blob/master/view/u256.go

    Note: this bug likely applies to any use of U256Str including the bid.value, etc.

    (2) I haven't looked into the cause here but suspect an issue with fastssz either with serializing the byte list or mixing in the bound in the correct way...

    opened by ralexstokes 5
  • Fix extra write to http writer

    Fix extra write to http writer

    Getting this in the logs for GetPayload:

    2022/05/13 15:39:11 http: superfluous response.WriteHeader call from main.(*responseWriter).WriteHeader (utils.go:34)
    
    opened by lightclient 5
  • Track fee recipients upon validator registration

    Track fee recipients upon validator registration

    I am doing some local integration testing using mergemock and it seems that the relay does not keep track of the validator's preferred fee recipient.

    The code should be updated here https://github.com/protolambda/mergemock/blob/master/relay.go#L196 to fix this.

    It seems from a quick pass this will need a map to track the pairs and also knowledge of the validator set to map public key to index (as each builder endpoint only gets one or the other but not both).

    The implementation could skip the validator set mapping as a stopgap if it only assumes one "session" at a time (cf. the latestPubkey in the relayBackend)

    edit: mergemock will pass the expected value if you supply it in a forkChoiceUpdated message

    opened by ralexstokes 2
  • Add eth_getBlockByHash/Number to engine API

    Add eth_getBlockByHash/Number to engine API

    Registers and enables eth API namespace with EngineBackend.

    For now only implements eth_getBlockByHash and eth_getBlockByNumber in limited fashion (no full transactions and no pending blocks).

    opened by Ruteri 2
  • Add missing clean rule

    Add missing clean rule

    When building this project, I encountered the following problem:

    $ make
    make: *** No rule to make target `clean', needed by `all'.  Stop.
    $ make all
    make: *** No rule to make target `clean', needed by `all'.  Stop.
    

    This is because the all rule requires clean but this rule does not exist.

    https://github.com/protolambda/mergemock/blob/ccac956b1abddd73670d0c8fa3d503586b1801b6/Makefile#L3

    Also, make targets phony. Not really necessary, but it doesn't hurt.

    opened by jtraglia 1
  • Adjust signing domain for RegisterValidator and GetHeader

    Adjust signing domain for RegisterValidator and GetHeader

    Update GetHeader to match the spec, should be verified against proposer signing domain.

    Reference: https://github.com/ethereum/builder-specs/blob/main/specs/builder.md#containers and https://github.com/ethereum/builder-specs/blob/main/specs/builder.md#signing

    opened by Ruteri 1
  • Fix bug in CI

    Fix bug in CI

    Seems to happen occasionally on both the unit tests and the integration test.

    SIGILL: illegal instruction
    PC=0xb660d7 m=5 sigcode=2
    signal arrived during cgo execution
    instruction bytes: 0xf3 0x4c 0xf 0x38 0xf6 0xdd 0x66 0x4d 0xf 0x38 0xf6 0xe1 0xc4 0x62 0xd3 0xf6
    
    goroutine 16 [syscall]:
    runtime.cgocall(0xb46620, 0xc000175750)
    	/opt/hostedtoolcache/go/1.18.1/x64/src/runtime/cgocall.go:157 +0x5c fp=0xc000175728 sp=0xc0001756f0 pc=0x4219fc
    github.com/supranational/blst/bindings/go._Cfunc_blst_keygen(0xc0000393e0, 0xc0000393c0, 0x20, 0x0, 0x0)
    	_cgo_gotypes.go:372 +0x45 fp=0xc000175750 sp=0xc000175728 pc=0x891745
    github.com/supranational/blst/bindings/go.KeyGen({0xc0000393c0, 0x20, 0x13bec40?}, {0x0, 0x0, 0xcb7e20?})
    	/home/runner/go/pkg/mod/github.com/supranational/[email protected]/bindings/go/blst.go:135 +0x9c fp=0xc000175790 sp=0xc000175750 pc=0x8941fc
    github.com/prysmaticlabs/prysm/crypto/bls/blst.RandKey()
    	/home/runner/go/pkg/mod/github.com/prysmaticlabs/[email protected]/crypto/bls/blst/secret_key.go:32 +0xae fp=0xc000175818 sp=0xc000175790 pc=0x8d5f8e
    main.(*ConsensusCmd).Run(0xc0000a5200, {0xe90330?, 0xc000318200?}, {0x0?, 0xc000322300?, 0x20?})
    	/home/runner/work/mergemock/mergemock/consensus.go:116 +0x356 fp=0xc000175950 sp=0xc000175818 pc=0xb17b56
    github.com/protolambda/ask.(*CommandDescription).Execute(0xc00009a980, {0xe90330, 0xc000318200}, 0x0?, {0xc000132140, 0x4, 0x4})
    	/home/runner/go/pkg/mod/github.com/protolambda/[email protected]/ask.go:574 +0x6e7 fp=0xc000175c60 sp=0xc000175950 pc=0xb02727
    github.com/protolambda/ask.(*CommandDescription).Execute(0xc00009a900, {0xe90330, 0xc000318200}, 0x0?, {0xc000132130, 0x5, 0x5})
    	/home/runner/go/pkg/mod/github.com/protolambda/[email protected]/ask.go:476 +0x1b3 fp=0xc000175f70 sp=0xc000175c60 pc=0xb021f3
    main.main.func2()
    	/home/runner/work/mergemock/mergemock/main.go:64 +0x85 fp=0xc000175fe0 sp=0xc000175f70 pc=0xb20e85
    runtime.goexit()
    	/opt/hostedtoolcache/go/1.18.1/x64/src/runtime/asm_amd64.s:1571 +0x1 fp=0xc000175fe8 sp=0xc000175fe0 pc=0x489301
    created by main.main
    	/home/runner/work/mergemock/mergemock/main.go:63 +0x1d1
    
    goroutine 1 [select]:
    main.main()
    	/home/runner/work/mergemock/mergemock/main.go:69 +0x245
    
    goroutine 7 [chan receive]:
    github.com/ethereum/go-ethereum/core.(*txSenderCacher).cache(0x0?)
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:63 +0x3b
    created by github.com/ethereum/go-ethereum/core.newTxSenderCacher
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:55 +0x75
    
    goroutine 8 [chan receive]:
    github.com/ethereum/go-ethereum/core.(*txSenderCacher).cache(0x0?)
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:63 +0x3b
    created by github.com/ethereum/go-ethereum/core.newTxSenderCacher
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:55 +0x75
    text/template/parse.(*lexer).run(0xc0001961b0)
    	/opt/hostedtoolcache/go/1.18.1/x64/src/text/template/parse/lex.go:236 +0x2a
    created by text/template/parse.lex
    	/opt/hostedtoolcache/go/1.18.1/x64/src/text/template/parse/lex.go:229 +0x1ca
    
    goroutine 22 [runnable]:
    github.com/ethereum/go-ethereum/core.newTxSenderCacher.func1()
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:55
    runtime.goexit()
    	/opt/hostedtoolcache/go/1.18.1/x64/src/runtime/asm_amd64.s:1571 +0x1
    created by github.com/ethereum/go-ethereum/core.newTxSenderCacher
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:55 +0x75
    
    goroutine 23 [runnable]:
    github.com/ethereum/go-ethereum/core.newTxSenderCacher.func1()
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:55
    runtime.goexit()
    	/opt/hostedtoolcache/go/1.18.1/x64/src/runtime/asm_amd64.s:1571 +0x1
    created by github.com/ethereum/go-ethereum/core.newTxSenderCacher
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/core/tx_cacher.go:55 +0x75
    
    goroutine 24 [runnable]:
    github.com/ethereum/go-ethereum/metrics.NewMeterForced.func2()
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/metrics/meter.go:71
    runtime.goexit()
    	/opt/hostedtoolcache/go/1.18.1/x64/src/runtime/asm_amd64.s:1571 +0x1
    created by github.com/ethereum/go-ethereum/metrics.NewMeterForced
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/metrics/meter.go:71 +0xc5
    
    goroutine 25 [runnable]:
    github.com/ethereum/go-ethereum/consensus/ethash.startRemoteSealer.func1()
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]917-87d642df3e87/consensus/ethash/sealer.go:263
    runtime.goexit()
    	/opt/hostedtoolcache/go/1.18.1/x64/src/runtime/asm_amd64.s:1571 +0x1
    created by github.com/ethereum/go-ethereum/consensus/ethash.startRemoteSealer
    	/home/runner/go/pkg/mod/github.com/!marius!van!der!wijden/[email protected]/consensus/ethash/sealer.go:263 +0x319
    
    goroutine 30 [runnable]:
    os/signal.loop()
    	/opt/hostedtoolcache/go/1.18.1/x64/src/os/signal/signal_unix.go:21
    created by os/signal.Notify.func1.1
    	/opt/hostedtoolcache/go/1.18.1/x64/src/os/signal/signal.go:151 +0x2a
    
    rax    0x76882064825eacc9
    rbx    0xea8700
    rcx    0xea86a0
    rdx    0x2b6cedcb87925c23
    rdi    0x7fdac6e76d70
    rsi    0x7fdac6e76cf0
    rbp    0xa927ec58accde979
    rsp    0x7fdac6e76b58
    r8     0xfffffffeffffffff
    r9     0x24d155bab99d4de5
    r10    0xbd68c647da15337
    r11    0x73e101ba300e2e2
    r12    0x1a9a59b6e3dfd32c
    r13    0x82c6e84dcba41755
    r14    0x4c53b4b266e739ce
    r15    0x0
    rip    0xb660d7
    rflags 0x10246
    cs     0x33
    fs     0x0
    gs     0x0
    
    opened by lightclient 1
  • Use bytes instead of int of domain type

    Use bytes instead of int of domain type

    Alternative to #37 -- I think it's better to keep the same format as the spec, e.g. 0x00000001 little-endian. This PR does that by making DomainType a [4]byte array instead of int.

    opened by lightclient 1
  • fix domain handling when signing

    fix domain handling when signing

    the current implementation writes 1 as little-endian which sets the "low" bit when in fact we want to set the "high" bit.

    this PR fixes this so that it aligns w/ the consensus-specs

    fwiw other Go impls like zrnt just use direct bytes which are copied into the domain, no need to bother with endianness

    opened by ralexstokes 1
  • Define the builder signing API in to include a signing domain and fork version etc.

    Define the builder signing API in to include a signing domain and fork version etc.

    from @protolambda:

    Define the signing API in a way where you are forced to include a signing domain and fork version etc.

    Signatures should be constructed in a way where they are unique for the purpose of the work and version of the protocol. See compute_signing_root and get_domain in the phase0 beacon spec.

    You essentially want to compute a "domain" (32 bytes) from a fork digest (constructed from genesis + fork info) and a type-domain (e.g. proposals and attestations have different domains to be sure they never have the same signature for two different intents). Or you can implement your own non-standard domain method. But definitely don't just create a BLS signature over data that might have a different meaning later on in the protocol. cc @lightclient

    Originally posted by @protolambda in https://github.com/protolambda/mergemock/pull/28#discussion_r871381698


    See also https://github.com/ethereum/consensus-specs/pull/2884 for the DomainType

    opened by metachris 1
Releases(v0.1.0)
  • v0.1.0(May 24, 2022)

    This release targets the Engine API v1.0.0-alpha.8 and the Builder spec v0.1.0.

    Change Log

    • Add support for batch validator registrations by @lightclient in https://github.com/protolambda/mergemock/pull/45
    • Track registrations by @lightclient in https://github.com/protolambda/mergemock/pull/46

    Full Changelog: https://github.com/protolambda/mergemock/compare/v0.0.0...v0.1.0

    Source code(tar.gz)
    Source code(zip)
  • v0.0.0(May 23, 2022)

Owner
Diederik Loerakker
Platform architect, specialized in Ethereum R&D. Building Eth2. Twitter: @protolambda
Diederik Loerakker
This testing tool surrounds go-ethereum with cannon to catch the blocks of retesteth going into go-ethereum and test cannon with them

Siege This testing tool surrounds go-ethereum with cannon to catch the blocks of retesteth going into go-ethereum and test cannon with them. Usage Sta

Diederik Loerakker 8 Mar 15, 2022
CLI tool to mock TCP connections. You can use it with Detox, Cypress or any other framework to automatically mock your backend or database.

Falso It is a CLI that allows you to mock requests/responses between you and any server without any configuration or previous knowledge about how it w

Sorin 3 Mar 15, 2022
Mock-the-fck - Mock exercise for human

Mock the fck Originally, Mockery-example Example case for mockery issue #128 fil

Mike Cat 0 Jan 21, 2022
HTTP mock for Golang: record and replay HTTP/HTTPS interactions for offline testing

govcr A Word Of Warning I'm in the process of partly rewriting govcr to offer better support for cassette mutations. This is necessary because when I

Seb C 131 Sep 27, 2022
A mock of Go's net package for unit/integration testing

netmock: Simulate Go network connections netmock is a Go package for simulating net connections, including delays and disconnects. This is work in pro

Lucas Wolf 1 Oct 27, 2021
mock server to aid testing the jaguar-java client API

stripe-mock stripe-mock is a mock HTTP server that responds like the real Stripe API. It can be used instead of Stripe's test mode to make test suites

knr.data 1 Dec 24, 2021
Powerful mock generation tool for Go programming language

Summary Minimock generates mocks out of Go interface declarations. The main features of minimock are: It generates statically typed mocks and helpers.

Juno Inc. 462 Sep 2, 2022
A tool that integrates SQL, HTTP,interface,Redis mock

Mockit 目标:将mock变得简单,让代码维护变得容易 分支介绍 main 主分支,覆盖了单元测试 light 轻分支,去除了单元测试,简化了依赖项,方便其他团队使用 常见Mock难点 不同中间件,mock库设计模式不一致,学习代价高,差异化明显 mock方案强依赖服务端,无法灵活解耦 单元测试

SHIHUO 13 Sep 22, 2022
siusiu (suite-suite harmonics) a suite used to manage the suite, designed to free penetration testing engineers from learning and using various security tools, reducing the time and effort spent by penetration testing engineers on installing tools, remembering how to use tools.

siusiu (suite-suite harmonics) a suite used to manage the suite, designed to free penetration testing engineers from learning and using various security tools, reducing the time and effort spent by penetration testing engineers on installing tools, remembering how to use tools.

Re 271 Sep 13, 2022
A yaml data-driven testing format together with golang testing library

Specimen Yaml-based data-driven testing Specimen is a yaml data format for data-driven testing. This enforces separation between feature being tested

Design it, Run it 0 Jan 31, 2022
Sql mock driver for golang to test database interactions

Sql driver mock for Golang sqlmock is a mock library implementing sql/driver. Which has one and only purpose - to simulate any sql driver behavior in

DATA-DOG 4.7k Sep 27, 2022
Mock object for Go http.ResponseWriter

mockhttp -- Go package for unit testing HTTP serving Unit testing HTTP services written in Go means you need to call their ServeHTTP receiver. For thi

Tv 21 Mar 22, 2022
ESME is a go library that allows you to mock a RESTful service by defining the configuration in json format

ESME is a go library that allows you to mock a RESTful service by defining the configuration in json format. This service can then simply be consumed by any client to get the expected response.

Sumit Tokkar 3 Mar 2, 2021
mockery - A mock code autogenerator for Golang

mockery - A mock code autogenerator for Golang

Vektra 3.9k Sep 26, 2022
Create your own mock server with just a JSON file!

Gmocker Run a blazing fast mock server in just seconds! ?? All you need is to make a json file that contains path and response mapping. See an example

Ananto 51 Jul 25, 2022
Create your own blazing fast mock server with just a JSON file!

Gmocker Run a blazing fast mock server in just seconds! ?? All you need is to make a json file that contains path and response mapping. See an example

Ananto 51 Jul 25, 2022
Vitaly Berg 7 Aug 10, 2021
Completely type-safe compile-time mock generator for Go

Mockc Mockc is a completely type-safe compile-time mock generator for Go. You can use it just by writing the mock generators with mockc.Implement() or

Geon Kim 30 Aug 25, 2022
A basic lightweight HTTP client for Go with included mock features.

A basic lightweight HTTP client for Go with included mock features. Features Support almost all http method like G

Andres Mijares 1 May 2, 2022