Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(params): UnmarshalChainJSONConfig and MarshalChainJSONConfig #92

Open
wants to merge 33 commits into
base: main
Choose a base branch
from

Conversation

qdm12
Copy link
Collaborator

@qdm12 qdm12 commented Dec 17, 2024

Why this should be merged

Allow wallet and other clients to parse blocks from both coreth & subnet-evm, and also make their genesis/chain-configs.

How this works

🕐 Waiting on feedback from Arr4n to make sure this is how it's supposed to operate before updating the function comment and associated test(s)

UnmarshalChainConfig is a standalone function func UnmarshalChainConfigJSON[T any](data []byte, config *ChainConfig, extra *T) (err error) taking as input arguments:

  • data []byte the json data to decode
  • config *ChainConfig the chain config pointer to decode to, except the json "extra" key, if non-nil
  • extra *T the extra destination pointer to decode the "extra" key object to, if non-nil

How this was tested

New unit test Test_UnmarshalChainConfig

@qdm12 qdm12 force-pushed the qdm12/params/unmarshal-config branch from 32afb0b to a41a78d Compare December 17, 2024 18:20
params/json.libevm.go Outdated Show resolved Hide resolved
params/json.libevm.go Outdated Show resolved Hide resolved
params/json.libevm.go Outdated Show resolved Hide resolved
params/json.libevm.go Outdated Show resolved Hide resolved
@@ -144,3 +145,60 @@ func TestChainConfigJSONRoundTrip(t *testing.T) {
})
}
}

func Test_UnmarshalChainConfig(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please only include _ in identifiers when it's required by a general convention; the only instance I can think of off the top of my head is the godoc parsing of testable examples.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _ right after Test can be removed indeed. However, if we test for example func (o *Object) myFunc() naming the test as TestObject_myFunc is better than TestObjectmyFunc readability-wise, would you agree or have another suggestion? 🤔

@@ -144,3 +145,60 @@ func TestChainConfigJSONRoundTrip(t *testing.T) {
})
}
}

func Test_UnmarshalChainConfig(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that the existing test cases will be sufficient if you refactor as I recommended above. That will also give us a greater degree of confidence between the situations where extras are explicitly registered vs merely passed to the function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are for the happy path for both the marshal and unmarshal functions. However, I prefer to have unit tests for each marshal and umarshal functions, on top of the existing test, to test unhappy paths/corner cases, especially given the any/generic thingies which are prone to error.

params/json.libevm.go Outdated Show resolved Hide resolved
params/json.libevm.go Outdated Show resolved Hide resolved
params/json.libevm_test.go Outdated Show resolved Hide resolved
@qdm12 qdm12 requested a review from ARR4N December 19, 2024 12:55
@qdm12 qdm12 marked this pull request as ready for review December 19, 2024 13:50
params/json.libevm.go Outdated Show resolved Hide resolved
@qdm12 qdm12 force-pushed the qdm12/params/unmarshal-config branch 2 times, most recently from 06c7610 to a6ebb69 Compare December 20, 2024 11:41
@qdm12 qdm12 force-pushed the qdm12/params/unmarshal-config branch from a6ebb69 to a46252d Compare December 20, 2024 14:29
switch {
case err != nil:
return fmt.Errorf("decoding root chain config: %s", err)
case extra == nil: // ignore the extra, whether at the root JSON depth, or at the "extra" JSON field.
Copy link
Collaborator Author

@qdm12 qdm12 Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to support this case where the user pass in a nil extra? 🤔 Or not really? Thinking about the case an extra set to nil is passed and we silently not decode the extra content

Copy link
Collaborator Author

@qdm12 qdm12 Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to return an error extra pointer argument is nil because:

  • I don't think there is a point using this function at all if the extra argument is a (typed) nil
  • Discarding silently the extra field sounds like a dangerous path, better to return an error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't want to support nil extras because that is never a valid use case. In the extras-registered scenario it will never be nil (by design), and in the non-registered scenario the caller should just use the regular JSON decoding mechanism of either json.Decode([]byte, *ChainConfig) or ChainConfig.UnmarshalJSON().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think returning an error in case the extra is nil makes sense in that case then.

Copy link
Collaborator

@darioush darioush left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, let's add MarshalJSON, we can do it in this or a separate PR

params/json.libevm_test.go Outdated Show resolved Hide resolved

testCases := map[string]struct {
jsonData string // string for convenience
extra any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: seems this is always set to &testExtra{} so we can remove it from the table struct and have the test instantiate it.
or if it is to keep things generic, there could be newExtra: func() any { return &testExtra{} }

Copy link
Collaborator Author

@qdm12 qdm12 Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it a bit to be extra any to be able to throw different things at it.

return json.Unmarshal(data, (*chainConfigWithoutMethods)(c))
}
extraConstructors := registeredExtras.Get()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(style) Please don't use such verbose variable names when the scope is only 3 lines; they distract from reading of the code. I'd personally be happy with just r, but understand that others are not—as long as it's not 5 syllables.

See style guide

Copy link
Collaborator Author

@qdm12 qdm12 Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read a few style guides including that one, my view on it, unless it's a really obvious use (i.e. i for a range index), it's nice to have a meaningful name almost always. For example c instead of extraConstructors made me wonder what was c and had to lookup where it was created and what .Get() was returning, and, because it's a generic function, had to dig into extraConstructors struct. Not a big deal obviously, but it does make readability worst in my opinion.

Obviously that's my opinion on this, so I'll conform to the existing code and use shorter variable names 😉

EDIT: also one more reason is I do like to use a single letter for the receiving struct for methods, so using single letter variables in there makes things confusing imo

EDIT2: my brain is kind of very formed (malformed or ok-formed, debatable 😄) to use explicit non-single-letter variable names, so feel free to hammer with style comments in case you feel like it should be a smaller variable name. I got the extraConstructors -> ec but after going through the rest of the code I wrote, I'm feeling like:

params/json.libevm.go Outdated Show resolved Hide resolved
params/json.libevm.go Outdated Show resolved Hide resolved
switch {
case err != nil:
return fmt.Errorf("decoding root chain config: %s", err)
case extra == nil: // ignore the extra, whether at the root JSON depth, or at the "extra" JSON field.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't want to support nil extras because that is never a valid use case. In the extras-registered scenario it will never be nil (by design), and in the non-registered scenario the caller should just use the regular JSON decoding mechanism of either json.Decode([]byte, *ChainConfig) or ChainConfig.UnmarshalJSON().

case extra == nil: // ignore the extra, whether at the root JSON depth, or at the "extra" JSON field.
case reuseJSONRoot:
err = json.Unmarshal(data, extra)
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(style) When a variable is scoped to a block, please scope it as such where possible, e.g. by initialisation within a if or switch statement.

if err := json.Unmarshal(...); err != nil {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, the short if forms makes it harder to read, but maybe that's just me eyes acting strange 😄
Regarding the scoping, we could change this to

err := json.Unmarshal(data, extra)
if err != nil {

? 🤔

// `data` is decoded into `config` and the extra is ignored.
func UnmarshalChainConfigJSON[T any](data []byte, config *ChainConfig, extra *T, reuseJSONRoot bool) (err error) {
err = json.Unmarshal(data, (*chainConfigWithoutMethods)(config))
switch {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a switch needed? Either we are reusing the JSON root or we're not, and each of those may or may not return an error.

See my comment below about double unmarshalling into the extra field when not reusing the root, which may have influenced this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The double unmarshaling is fixed, and switch converted to 2 ifs which are indeed perhaps clearer 😉

require.NoError(t, json.Unmarshal([]byte(tt.jsonInput), got))
require.Equal(t, tt.want, got)
})
expectedEncoded := bytes.NewBuffer(nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change to the original test needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just going over the existing test, and was wondering why we had subtests where it seems to me this makes it more convoluted? Maybe I missed something though. I can revert it of course, no worry.

"invalid_json": {
extra: &testExtra{},
expectedExtra: &testExtra{},
errMessage: "decoding root chain config: unexpected end of JSON input",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change-detector test, which results in having to change tests when implementations change.

See: https://google.github.io/styleguide/go/decisions#test-error-semantics

If you want to confirm precise error-spawning pathways then please either compare against a set of error variables (probably most appropriate here as along as they're not exported) or, in more complex situations, with specific error types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I'm aware 😄 I do test error messages, since I have seen many error messages that were unclear and/or repeating themselves, and I found testing error strings do reduce a lot of this from happening.

I don't think testing error variables (except nil vs non nil) help really, especially since we are focusing on "human readable" and not programmatically handled errors - we don't wrap errors with %w. But I can change this to check for nil vs non-nil only.

}
}

func TestUnmarshalChainConfigJSON(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. What cases aren't covered by the original tests? (Other than errors, that is)
  2. Can the non-erroring cases just be added to the table above?

Re (1); while coverage isn't a perfect metric, I'm seeing full happy-path coverage on your branch with just the originals.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #92 (comment) 😉

jsonData string // string for convenience
extra *testExtra
reuseJSONRoot bool
expectedConfig ChainConfig
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of libevm uses "want" instead of "expected" and "got" instead of "actual".

Copy link
Collaborator Author

@qdm12 qdm12 Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed expected prefixes to want prefixes.
However - ha I want shorter variable names perhaps - regarding actual (and got), I think i.e. data, ok := MarshalXYZ in a test is better than actualData, actualOk := MarshalXYZ and there is no need for an actual prefix really? And if it's only one variable being checked, why not name it to what it actually is i.e. data instead of actual

@qdm12 qdm12 force-pushed the qdm12/params/unmarshal-config branch from da45de2 to 2172aa7 Compare December 23, 2024 12:21
@qdm12 qdm12 changed the title feat(params): UnmarshalChainConfig function feat(params): UnmarshalChainJSONConfig and MarshalChainJSONConfig Dec 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants