Skip to content

Provide -filter and -override with command line generator #106

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

Merged
merged 1 commit into from
Jul 17, 2024
Merged

Provide -filter and -override with command line generator #106

merged 1 commit into from
Jul 17, 2024

Conversation

dengsh12
Copy link
Contributor

@dengsh12 dengsh12 commented Jul 11, 2024

Proposed changes

In #101, we provided a command line generator for support files. The generator creates a directives map and matchFunc based on the directives definition in the source code.

However, some directives in source codes are outdated or unofficial (like post_action in OSS). You may only want to include some official directives in output and we provide a -filter option to achieve this.

Some directives have different definition in crossplane vs source code(like if in OSS, we added ngxConfExpr to it). You may want to override the output and we provide a -override option to achieve this.

Checklist

Before creating a PR, run through this checklist and mark each as complete.

  • I have read the CONTRIBUTING document
  • If applicable, I have added tests that prove my fix is effective or that my feature works
  • If applicable, I have checked that any relevant tests pass after adding my changes
  • I have updated any relevant documentation (README.md)

@dengsh12 dengsh12 requested a review from a team as a code owner July 11, 2024 18:13
Comment on lines +20 to +26
tests := []struct {
name string
input string
expected overrideItem
wantErr bool
}{
{
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: there's another idiom for table-driven tests that uses a map to specify the name of the test case, I think that reads a little better:

	testcases := map[string]struct{
		input    string
		expected overrideItem
		wantErr  bool
	}{
		"normalFormat_pass": {
			input: "location:ngxHTTPMainConf|ngxConfTake12,ngxStreamMainConf",
			expected: overrideItem{
				directive: "location",
				masks: []generator.Mask{
					{"ngxHTTPMainConf", "ngxConfTake12"},
					{"ngxStreamMainConf"},
				},
			},
		},
		// <other test cases omitted for brevity>
	}
	for name, tc := range testcases{
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			
			// <test omitted for brevity>
		})
	}}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah, this one is better and align with other ut in crossplane. Applied.

Copy link
Member

Choose a reason for hiding this comment

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

@ryepup Many crossplane tests use a slice, but I agree that a map is better. You start to see hints of this when it becomes necessary to add a "name" or other key to the testcase struct.

@dengsh12 A more important benefit to using a map vs slice is that Go randomizes the order of keys when ranging over the map. I believe-- I do not have documentation that states this as fact on hand at the moment-- this is to dissuade anyone from getting used to maps being ordered in the event the internal details are changed. You can generally use this to your benefit as it will randomize the order the tests are run which can help to detect any unintentional shared memory that leaks through tests in the form of test flake.

Comment on lines 77 to 78
var parser overrideItem
err := parser.UnmarshalText([]byte(tc.input))
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: naming: Golang likes short variables names (see https://go.dev/wiki/CodeReviewComments#variable-names), in tests you'll often see two variables want and got to represent what the test wanted to the code to return, and what was actually returned.

Recommend using something besides "parser"

Suggested change
var parser overrideItem
err := parser.UnmarshalText([]byte(tc.input))
var got overrideItem
err := got.UnmarshalText([]byte(tc.input))

or maybe o

Suggested change
var parser overrideItem
err := parser.UnmarshalText([]byte(tc.input))
var o overrideItem
err := o.UnmarshalText([]byte(tc.input))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Get it. Applied

Comment on lines 80 to 90
if !tc.wantErr && err != nil {
t.Fatal(err)
}

if tc.wantErr && err == nil {
t.Fatal("expected error, got nil")
}

// If the testcase wants an error and there is an error, skip the output file validation.
// Output makes no sense when there is an error.
if err != nil {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: if we're using the require library, it has some helpers to assert around errors. That can let us eliminate the leftover if err != nil

Suggested change
if !tc.wantErr && err != nil {
t.Fatal(err)
}
if tc.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
// If the testcase wants an error and there is an error, skip the output file validation.
// Output makes no sense when there is an error.
if err != nil {
return
}
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome! Thanks for telling me this. Applied

README.md Outdated
@@ -74,7 +74,9 @@ func main() {
```

# Generate support for third-party modules
This is an example that takes the path of a third-party module source code to generate support for it. Assume the source code path of that module is `./src`. You can call `go run cmd/generate/main.go ./src`. The stdout will be like
This is a simple example that takes the path of a third-party module source code to generate support for it. For detail usage of the tool, please run
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
This is a simple example that takes the path of a third-party module source code to generate support for it. For detail usage of the tool, please run
This is a simple example that takes the path of a third-party module source code to generate support for it. For detailed usage of the tool, please run

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Applied

README.md Outdated
Comment on lines 78 to 79
`go run ./cmd/generate/ --help`
Assume the source code path of that module is `./src`. You can call `go run ./cmd/generate/ --src-path=./src`. The stdout will be like
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`go run ./cmd/generate/ --help`
Assume the source code path of that module is `./src`. You can call `go run ./cmd/generate/ --src-path=./src`. The stdout will be like
`go run ./cmd/generate/ --help`. Assuming the source code path of that module is `./src`, you can call `go run ./cmd/generate/ --src-path=./src`. The output will be similar to:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Applied

Comment on lines 22 to 32
func (reader *filterFlag) Set(value string) error {
if reader.filter == nil {
reader.filter = make(map[string]struct{})
}
reader.filter[value] = struct{}{}
return nil
}

func (reader *filterFlag) String() string {
return fmt.Sprintf("%v", *reader)
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: no longer a "Reader"

Suggested change
func (reader *filterFlag) Set(value string) error {
if reader.filter == nil {
reader.filter = make(map[string]struct{})
}
reader.filter[value] = struct{}{}
return nil
}
func (reader *filterFlag) String() string {
return fmt.Sprintf("%v", *reader)
}
func (f *filterFlag) Set(value string) error {
if f.filter == nil {
f.filter = make(map[string]struct{})
}
f.filter[value] = struct{}{}
return nil
}
func (f *filterFlag) String() string {
return fmt.Sprintf("%v", *f)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure! Applied. Should've checked all the usages after rename...

}

func (item *overrideItem) UnmarshalText(text []byte) error {
rawOverride := string(text)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: The bytes package may offer all of the functions from the strings package that you may be able to use to avoid the (very small) cost of casting []byte to string

Copy link
Contributor Author

@dengsh12 dengsh12 Jul 17, 2024

Choose a reason for hiding this comment

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

Oh, really surprised to see bytes provide such strong capabilities. Thanks for telling me this. But for here I think the cost is acceptable? Transfer it into string seems more make sense(since override is a string originally).


item.directive = directive
if directive == "" {
return errors.New("directive name is empty")
Copy link
Member

Choose a reason for hiding this comment

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

Include the override to communicate which one is being rejected?

return fmt.Errorf("directive name is empty: override=%s", rawOverride)

I think I would do this for most errors where it would be helpful to see the specific input being rejected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We moved it to Set to simplify the errors in item.UnmarshalText
#106 (comment)

func (ov *override) Set(value string) error {
	if *ov == nil {
		*ov = override{}
	}
	var item overrideItem
	err := item.UnmarshalText([]byte(value))
	if err != nil {
		return fmt.Errorf("invalid override %s:%w", value, err)
	}

	(*ov)[item.directive] = item.masks
	return nil
}

// rawStr should follow the format: directive:bitmask00|bitmask01|...,bitmask10|bitmask11|...
directive, definition, found := strings.Cut(rawOverride, ":")
if !found {
return fmt.Errorf("colon not found, override=%s, ", rawOverride)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure why you've added an extra comma and space?

Suggested change
return fmt.Errorf("colon not found, override=%s, ", rawOverride)
return fmt.Errorf("colon not found, override=%s", rawOverride)

If you're concerned that printing the override could look ambiguous, you could use %q to wrap the string in quotes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#106 (comment). Sorry for omitting removal of rawOverride output here. Just removed

Comment on lines +88 to +90
if *ov == nil {
*ov = override{}
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: These patterns always give me pause. I think calling a method on a nil value is generally a programming error where a panic is warranted, but there are reasons to do this. I'm not sure that it makes sense here though. I think you want the caller to initialize the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went in this way because most of the situations user doesn't provide a -override, lazy initialization can avoid unnecessary allocation(and Set is not run in parallel so lazy initialization is safe here).

var item overrideItem
err := item.UnmarshalText([]byte(value))
if err != nil {
return fmt.Errorf("invalid override %s, reason:%w", value, err)
Copy link
Member

Choose a reason for hiding this comment

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

Errors more commonly follow this idiom:

Suggested change
return fmt.Errorf("invalid override %s, reason:%w", value, err)
return fmt.Errorf("invalid override %s: %w", value, err)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Get it, applied.

@dengsh12 dengsh12 merged commit 4a65ead into nginxinc:main Jul 17, 2024
2 checks passed
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