-
Notifications
You must be signed in to change notification settings - Fork 8
Te 4789 add plan command #374
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
Conversation
|
|
||
| func logErrorAndExit(err error) { | ||
| exitError := new(exec.ExitError) | ||
| fmt.Fprintln(os.Stderr, err) |
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 was accidentally removed during the review of #371 😅
Without this line logErrorAndExit becomes andExit!
internal/command/plan.go
Outdated
| // Structure of the JSON that is output when running `bktec plan`. | ||
| type TestPlanSummary struct { | ||
| Identifier string `json:"BKTEC_PLAN_IDENTIFIER"` | ||
| Parallelism string `json:"BKTEC_PARALLELISM"` |
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.
Parallelism is strictly an int not a string, and it's value is converted from an int representation in testPlan.Parallelism on line 68.
It's represented as a string here because this struct is output to STDOUT in JSON format with the intention that it be piped into buildkite-agent env set --input-format=json -, which requires string keys and values.
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 wonder if a shorter version of this comment be made into a code 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.
Yeh, sounds good. Will update.
| // createRequestParam generates the parameters needed for a test plan request. | ||
| // For runners other than "rspec", it constructs the test plan parameters with all test files. | ||
| // For the "rspec" runner, it filters the test files through the Test Engine API and splits the filtered files into examples. | ||
| func createRequestParam(ctx context.Context, cfg config.Config, files []string, client api.Client, runner TestRunner) (api.TestPlanParams, 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 method is moved from command.Run as it's now used in both command.Run and command.Plan.
Apart from the addition of MaxParallelism it's unchanged.
| } | ||
| } | ||
|
|
||
| func getTestFilesFromFile(path string) ([]string, 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 method is moved unchanged from command.Run as it's now used in both command.Run and command.Plan.
| @@ -0,0 +1,86 @@ | |||
| package command_test | |||
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.
As this is conceptually a "black box" test I've put it in a different package so it can only test the exported methods of command.Plan.
|
|
||
| source "https://rubygems.org" | ||
|
|
||
| gem "rspec" |
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.
The new test in internal/command/run_test.go calls rspec --dry-run during the plan generation, so needs a Gemfile to be present.
| // TestPlan represents the entire test plan. | ||
| type TestPlan struct { | ||
| Identifier string `json:"identifier"` | ||
| Parallelism int `json:"parallelism"` |
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.
These fields are already present in the JSON response from the test plan API, but were not declared in this struct.
| Value: 0, | ||
| Usage: "instruct the test planner to calculate optimal parallelism for the build, not to exceed the provided value. When 0 this flag is ignored and the test plan parallelism will be derived from the BUILDKITE_PARALLEL_JOB_COUNT environment variable", | ||
| Action: func(ctx context.Context, cmd *cli.Command, v int) error { | ||
| if v < 0 || v > 1000 { |
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.
The API for max parallelism has this same constraint.
| env := env.OS{} | ||
|
|
||
| debug.SetDebug(env.Get("BUILDKITE_TEST_ENGINE_DEBUG_ENABLED") == "true") | ||
| debug.SetOutput(os.Stderr) |
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.
Is this intentional? If yes, is it because we want the output the plan summary stdout?
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.
Yes it's intentional, because the output on STDOUT for this command is intended to be parsed. If we "cross the streams" the output won't be valid JSON:
DEBUG: some message
{"BKTEC_PARALLELISM":"5","BKTEC_PLAN_IDENTIFIER":"abc123/zxy987"}I'll probably pitch for all debug to go to STDERR at some point, although I see looking at the git history this has flip-flopped over time between STDOUT / STDERR so I imagine there is some background there.
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 can't remember why we flip-flopped from streaming the debug to stdout/stderr, but it will be nice if they are being streamed to the same place.
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.
Here's the origin story #85 (comment)
I think we should codify that outputting to stdout by default instead of stderr is an explicit decision to conform to 12-factor principles.
Requiring everything to go to STDOUT will require a complete rethink of the dynamic parallelism mechanism so I'm pretty keen not to maintain that restriction.
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 for digging. Outputing the log to stderr for plan command is fine to me. We might need to redesign the whole bktec logging/debugging at some point.
| var planWriter io.Writer = os.Stdout | ||
|
|
||
| // Structure of the JSON that is output when running `bktec plan`. | ||
| type TestPlanSummary struct { |
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.
Is this a placeholder or the final structure we want for triggering the dynamic pipeline? We’re using BUILDKITE_TEST_ENGINE_XX for other environment variables, so I’m wondering if using BKTEC_XX would be inconsistent.
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.
Consistency seems desirable.
My understanding these are temporary env vars that exist only for the purposes for generating the dynamic pipeline steps so perhaps we want to differentiate these from the other BUILDKITE_TEST_ENGINE_XX env vars?
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.
BUILDKITE_TEST_ENGINE_XX sounds good, I'll update the naming.
| } | ||
|
|
||
| // By default command.Plan writes to os.Stdout but the output can be changed here. | ||
| func SetPlanWriter(writer io.Writer) { |
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.
any reason this has to be public? If it's just for testing, we don't need to make it public because the test lives in the same package.
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'll "unexport" it.
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.
@nprizal the test actually isn't in the same package, see my comment at the top of the test file about it being a "black box" test.
Would you prefer I leave the SetPlanWriter method exported, or move the test into package plan?
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 the question is whether we want the consumer to set/change the writer or not. If this is solely for testing purposes, I'd rather have to keep it private and use other way to test the output. But if we expect the consumer to set/change the writer, then making it public is a way to go.
internal/command/run.go
Outdated
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.
[nit] I think we can move this to request_param.go as we call this fn as part of param creation process.
nprizal
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.
Tested locally and it works. But I have a question around the test plan summary structure and env vars key.
The other thing I noticed that the default error message for invalid option value is a bit cryptic. I wonder if we can customize the error message
Incorrect Usage: invalid value "foobar" for flag -max-parallelism: strconv.ParseInt: parsing "foobar": invalid syntax
invalid value "foobar" for flag -max-parallelism: strconv.ParseInt: parsing "foobar": invalid syntax
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.
Comparing this file to run.go was useful for my review.
| package command |
Will we add debugging statements later?
Is it worth adding TODOs about handling server errors and connectivity issues gracefully?
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.
Will we add debugging statements later?
@gchan I think most of the debug output is generated in the methods that are called from this method. E.g. command.Plan calls createRequestParams and the latter has debug statements. Did you have something else in mind?
Is it worth adding TODOs about handling server errors and connectivity issues gracefully?
The apiClient calls down api.Client.doWithRetry which has some error handling, did you have something more in mind?
test-engine-client/internal/api/client.go
Lines 96 to 102 in d17feb2
| // DoWithRetry sends http request with retries. | |
| // Successful API response (status code 200) is JSON decoded and stored in the value pointed to by v. | |
| // The request will be retried when the server returns 429 or 5xx status code, or when there is a network error. | |
| // After reaching the retry timeout, the function will return ErrRetryTimeout. | |
| // The request will not be retried when the server returns 4xx status code, | |
| // and the error message will be returned as an error. | |
| func (c *Client) DoWithRetry(ctx context.Context, reqOptions httpRequest, v interface{}) (*http.Response, 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.
Nothing extra to add regarding debugging output!
Regarding error handling, I was wondering if we have considered what this may look like should we support an offline/fallback mode and whether we should add any code comments.
|
Looks good to me (as someone new to bktec and refreshing on GoLang). Reviewing commit-by-commit was easy and helped me understand the changes better, thanks! |
Extract this duplicate behaviour from command.Run and command.Plan into a separate func and file.
The `log` part of `logErrorAndExit` was accidentally removed in a recent change.
This method is used in both `command.Run` and `command.Plan`.
8d64b31 to
1ba62a6
Compare
@nprizal thanks, I've updated these if you wouldn't mind having another look.
I don't see a way to do this, I'll have more of a dig. |
nprizal
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.
Looks good! I’ll leave the decision around making the SetPlanWriter public or private to you.
I think I'll leave it exported, I'll probably remove the method entirely in #379 |
This changes adds a new command,
bktec plan [...], which generates a test plan without running any tests.The command prints select metadata about the plan to
STDOUT:This output format is intended to be used as input for
buildkite-agent env sete.g.: