-
Notifications
You must be signed in to change notification settings - Fork 54
feat: format v3 #249
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
base: main
Are you sure you want to change the base?
feat: format v3 #249
Conversation
Format v3 uses a map instead of a list for `essential` and deprecates the field `v3-essential` which is only used during the transition from v2 to v3.
|
niemeyer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a first pass, as detailed below it's surprising and unfortunate that this PR seems to make things significantly worse almost everywhere it touches. This was supposed to be a cleanup PR ideally.
internal/setup/yaml.go
Outdated
| }, | ||
| } | ||
|
|
||
| func parseSliceEssential(yamlPkg *yamlPackage, yamlSlice *yamlSlice, slice *Slice) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This refactoring makes it hard to track what changed compared to the previous implementation. Could we do the extraction in a dedicated function in another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that but it is a bit overkill because what happens if we end up changing the approach, then we merged a PR for a refactor that wasn't actually fully needed. As I told you offline, this function needs a bit of love to make it more readable but, if done properly, I think it is easy to see the logic just moved. I will make the changes and we can discuss.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with the latest changes it is easier to see that the diff is the same.
|
As discussed with @niemeyer this week and the previous one, I have changed the PR. The current approach does not need two separate definitions of the same type and instead just changes how the conflicting field is parsed (in > v3 as a map, in v1, v2 as a list). This solution is not as strict as the previous one in regards to The complexity that still exists in the current approach is mainly because we need to produce good error messages and discern between all combinations of format + fields. For the record, Paul and me spent even more time this week thinking about the problem and we also tried:
|
upils
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @letFunny. I have a couple of questions, mainly about tests, but overall it LGTM.
| // 'v3-essential' to the shape expected by the v3 format. | ||
| // skip is set to true when an accurate translation of the test is not | ||
| // possible, for example having duplicates in the list. | ||
| func oldEssentialToV3(c *C, input []byte) (out string, skip bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick]: This is a clever solution to avoid duplicating the test cases but given the complexity I find it a bit hard to know what the final input to the test is.
I also wonder if we really need to rerun this many tests when many of them are not testing the essential resolution logic. I guess we are better safe than sorry but I wonder how hard that will be to maintain, especially when adding new tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, related to my earlier comment above. I would like to do the opposite: keep tests compatible with the latest version, and backport it to the old ones. Eventually we can just drop the backports.
Or, alternatively, drop this whole multi-version logic and have just specific tests that cover the difference. Yeah, that's probably better actually.
niemeyer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good, thank you. A few comments:
| Packages map[string]*Package | ||
| Archives map[string]*Archive | ||
| Maintenance *Maintenance | ||
| // Format is used for parsing and error checking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is documenting the type, so the focus here is on what it actually is, not where it is used. It is going to be used wherever it is needed, and this information will be out of date soon.
The Format must also be moved as the first item as everything else follows from it.
| }, | ||
| release: &setup.Release{ | ||
| Format: "v1", | ||
| Archives: map[string]*setup.Archive{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per above note, we always end up thinking as Format as the first thing.
| `, | ||
| }, | ||
| relerror: `cannot add slice to itself as essential "mypkg_slice1" in slices/mydir/mypkg.yaml`, | ||
| relerror: `package "mypkg": cannot add slice to itself as essential "mypkg_slice1"`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please not fiddle with unrelated messages unless it's clearly a fix or improvement? Some of these messages have been extensively discussed, and this is one example where it is clearly making it significantly worse and out of convention, and this is all a distraction given the main topic of the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention was to make them fit with the convention I saw elsewhere in the code as we have slightly different error messages when "decoding" from yaml vs when we are parsing or validating the contents. But you are right here, let's not discuss it as part of this PR.
| runParseReleaseTests(c, v2FormatTests) | ||
|
|
||
| // Run tests for "v3" format. | ||
| v3FormatTests := make([]setupTest, 0, len(setupTests)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a follow up PR, can we please invert this whole logic so that the tests are actually reflecting v3, and then we fiddle backwards instead of forwards? Otherwise we keep adding more work to the tip, which is where we care the most about, and eventually we'll have an issue because we'll want to drop v1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline: agree on testing v3. About the other point of manually testing, it comes a point where the testing code is so complex that we are not sure it is correct anymore. We can rely on the knowledge about the difference between v1, v2 and v3.
| // 'v3-essential' to the shape expected by the v3 format. | ||
| // skip is set to true when an accurate translation of the test is not | ||
| // possible, for example having duplicates in the list. | ||
| func oldEssentialToV3(c *C, input []byte) (out string, skip bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, related to my earlier comment above. I would like to do the opposite: keep tests compatible with the latest version, and backport it to the old ones. Eventually we can just drop the backports.
Or, alternatively, drop this whole multi-version logic and have just specific tests that cover the difference. Yeah, that's probably better actually.
| m := map[string]*yamlEssential{} | ||
| es.isList = false | ||
| err := value.Decode(&m) | ||
| if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have the yaml.Node, so we should be able to tell whether it has the right type or not without assuming that we understand what the arbitrary error is. This should allow us to switch on the type and have more precise errors.
| es.isList = true | ||
| for _, sliceName := range l { | ||
| if _, ok := m[sliceName]; ok { | ||
| return fmt.Errorf("cannot decode essential: repeats %s", sliceName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a decoding error. We had a nice message before. Let's please make sure the entire composition is still good and clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline, this is not a decoding error. Find the original error message and adapt it.
| V3Essential map[string]*yamlEssential `yaml:"v3-essential,omitempty"` | ||
| // For backwards-compatibility reasons with v1 and v2, essential needs | ||
| // custom logic to be parsed. See [yamlEssentialListMap]. | ||
| Essential yamlEssentialListMap `yaml:"essential,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's please keep the original order, and give the decoding one the odd name. Essential should be what we want to think of the essential field when we're dealing with it on v3.
| } | ||
| } else { | ||
| if yamlPkg.V3Essential != nil { | ||
| return nil, fmt.Errorf("cannot parse package %q: v3-essential is deprecated since format v3", pkgName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/deprecated/obsolete/
Deprecation is typically something that still works but shouldn't be used.
| return fmt.Errorf("package %q has invalid essential slice reference: %q", yamlPkg.Name, refName) | ||
| } | ||
| if sliceKey.Package == slice.Package && sliceKey.Slice == slice.Name { | ||
| return fmt.Errorf("package %q: cannot add slice to itself as essential %q", yamlPkg.Name, refName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the refName here not a %s? Those are slices, right? So other than validating the slice name itself, which is done above, we can trust it to be right. That's unlike package name, which needs %q as it may be a word and get confused in the sentence.
niemeyer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments are above.
Format v3 uses a map instead of a list for
essentialand deprecates the fieldv3-essentialwhich is only used during the transition from v2 to v3.