Skip to content

feat(cli): add golden tests for errors (#11588) #12698

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 2 commits into from
Apr 1, 2024

Conversation

elasticspoon
Copy link
Contributor

@elasticspoon elasticspoon commented Mar 21, 2024

Fixes #11588

Creates golden files from coder/cli/errors.go.
Adds a unit test to test against golden files.
Adds a make file command to regenerate golden files.
Abstracts test against golden files.

Most of the error printing is handled by cli.prettyErrorFormatter and prettyErrorFormatter.format(err), neither is exported. I wanted to add tests in root_internal_test.go but that resulted in a circular dependency since it needed clitest.

I solved this issue by creating an export_cli_test.go file in which I exported those values. Since it is an _test file its exports will limited to cli_test package files.

Exported the formatted for testing purposes.

@cdr-bot cdr-bot bot added the community Pull Requests and issues created by the community. label Mar 21, 2024
@elasticspoon elasticspoon marked this pull request as ready for review March 21, 2024 06:33
@kylecarbs kylecarbs requested a review from mafredri March 21, 2024 12:10
@mafredri
Copy link
Member

Thanks for the PR @elasticspoon. There seems to be a data race, caught by CI, would you mind taking a look at that?

@elasticspoon
Copy link
Contributor Author

Fixed

@github-actions github-actions bot added the stale This issue is like stale bread. label Mar 29, 2024
@Emyrk
Copy link
Member

Emyrk commented Mar 29, 2024

I solved this issue by creating an export_cli_test.go file in which I exported those values. Since it is an _test file its exports will limited to cli_test package files.

Honestly, if it simplifies the imports, just export newPrettyErrorFormatter and prettyErrorFormatter. It is in root.go which is only imported by main. I don't think expanding the API service of essentially the main package is much of a worry.

Copy link
Member

@Emyrk Emyrk left a comment

Choose a reason for hiding this comment

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

This all looks good 👍. Was just talking about getting this in, thanks!

Let's just export the original prettyErrorFormatter and the new. If we can import that directly, it'll make code navigation easier.

You will have to rebase on main to get the latest error change update as well.

Ping me with any updates, happy to fast track this in 👍

@elasticspoon

Comment on lines 29 to 50
var cases []commandErrorCase

ExtractCommandPathsLoop:
for _, cp := range extractCommandPaths(nil, rootCmd.Children) {
name := fmt.Sprintf("coder %s", strings.Join(cp, " "))
// space to end to not match base exp example-error
if !strings.Contains(name, "exp example-error ") {
continue
}
for _, tt := range cases {
if tt.Name == name {
continue ExtractCommandPathsLoop
}
}
cases = append(cases, commandErrorCase{Name: name, Cmd: cp})
}
Copy link
Member

Choose a reason for hiding this comment

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

Searching sub commands is very annoying at present, I might make a PR to improve this experience in serpent. Until then though, can we do something a bit more exact? It feels strange to match a string like this.

We can do something like this:

	// Walk all commands and find the `example-error` subcommand.
	// Unfortunately, this cannot abort early, but it's not that huge of
	// a list to walk.
	var exampleErrorRootCmd *serpent.Command
	rootCmd.Walk(func(command *serpent.Command) {
		if command.Name() == "example-error" {
			exampleErrorRootCmd = command
		}
	})
	require.NotNil(t, exampleErrorRootCmd, "example-error command not found")

	var cases []commandErrorCase
ExtractCommandPathsLoop:
	for _, cp := range extractCommandPaths(nil, exampleErrorRootCmd.Children) {
		name := fmt.Sprintf("coder %s", strings.Join(cp, " "))
		for _, tt := range cases {
			if tt.Name == name {
				continue ExtractCommandPathsLoop
			}
		}
		cases = append(cases, commandErrorCase{Name: name, Cmd: cp})
	}

Creates golden files from `coder/cli/errors.go`.
Adds a unit test to test against golden files.
Adds a make file command to regenerate golden files.
Abstracts test against golden files.

fix: prevent data race

refactor: make formatter public
Parse error command in a less brittle way
@elasticspoon
Copy link
Contributor Author

@Emyrk addressed the issues. And exporting the formatted did clean it up a decent amount. Only other concern I have is:

if tt.Name == "coder exp example-error multi-multi-error" {
  inv.Stdin = os.Stdin
}

I have that line in my test because the multi-multi error does:

{
Use:   "multi-multi-error",
Short: "This is a multi error inside a multi error",
Handler: func(inv *serpent.Invocation) error {
  // Closing the stdin file descriptor will cause the next close
  // to fail. This is joined to the returned Command error.
  if f, ok := inv.Stdin.(*os.File); ok {
  _ = f.Close()
  }
  
  return errors.Join(
    xerrors.Errorf("first error: %w", errorWithStackTrace()),
    xerrors.Errorf("second error: %w", errorWithStackTrace()),
  )
},

I would like to change the example error to something like:

return errors.Join(
  xerrors.Errorf("parent error: %w", errorWithStackTrace()),
  errors.Join(
	  xerrors.Errorf("child first error: %w", errorWithStackTrace()),
	  xerrors.Errorf("child second error: %w", errorWithStackTrace()),
  ),
)

but I am unsure if this changes the meaning of what is being tested.

Is the example test only for the rendering of error or should it also be testing that the errors get joined together correctly?

@elasticspoon elasticspoon requested a review from Emyrk March 29, 2024 19:24
@Emyrk
Copy link
Member

Emyrk commented Mar 29, 2024

@elasticspoon Change the test to what you suggested.

> ```go
> return errors.Join(
>   xerrors.Errorf("parent error: %w", errorWithStackTrace()),
>   errors.Join(
> 	  xerrors.Errorf("child first error: %w", errorWithStackTrace()),
> 	  xerrors.Errorf("child second error: %w", errorWithStackTrace()),
>   ),
> )
> ```

but I am unsure if this changes the meaning of what is being tested.

It's all fine imo. The example-error is purely for testing error formatting. If there is a reason to bring back the stdin.Close(), we can bring it back later.

Make that change, drop the condition, and I'll green check this 👍

Change multi-multi-error do not produce an error
using a second close of os.Stdin
@elasticspoon
Copy link
Contributor Author

@Emyrk done

@github-actions github-actions bot removed the stale This issue is like stale bread. label Mar 30, 2024
@Emyrk Emyrk merged commit cfb9428 into coder:main Apr 1, 2024
@Emyrk
Copy link
Member

Emyrk commented Apr 1, 2024

Merged! Thanks @elasticspoon !

@github-actions github-actions bot locked and limited conversation to collaborators Apr 1, 2024
@elasticspoon elasticspoon deleted the error-golden-tests branch April 1, 2024 17:52
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
community Pull Requests and issues created by the community.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make golden files for pretty printing cli errors
3 participants