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

Add IssueFileCommand & update Set Env, Add Path to use latest standard #11

Merged
merged 7 commits into from Oct 20, 2020

Conversation

ashutoshgngwr
Copy link
Contributor

All the necessary details for the change is well documented in #9 but the overall gist of it can be found in this document alone.

@ashutoshgngwr
Copy link
Contributor Author

ashutoshgngwr commented Oct 15, 2020

Weird! Tests are passing on my machine, failing in CI.


Also works in a clean Docker container.


Sorry my bad, these tests rely on the GITHUB_ENV and GITHUB_PATH variables which is being provided by the CI environment. Let me fix it.

actions.go Outdated
if err != nil {
return fmt.Errorf(errFileCmdFmt, err)
}

Copy link
Owner

Choose a reason for hiding this comment

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

Need to defer closing the file. Also need to sync and flush with error handling. Alternatively, use ioutil.WriteFile

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 missed it! I think ioutil.WriteFile sounds cleaner. I'll fix this!

actions.go Show resolved Hide resolved
@@ -208,7 +259,8 @@ func (c *Action) Warningf(msg string, args ...interface{}) {
}

// WithFieldsSlice includes the provided fields in log output. "f" must be a
// slice of k=v pairs. The given slice will be sorted.
// slice of k=v pairs. The given slice will be sorted. It panics if any of the
Copy link
Owner

Choose a reason for hiding this comment

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

Thanks!

@@ -73,13 +116,51 @@ func TestAction_RemoveMatcher(t *testing.T) {
func TestAction_AddPath(t *testing.T) {
t.Parallel()

const envGitHubPath = "GITHUB_PATH"
Copy link
Owner

Choose a reason for hiding this comment

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

It's generally a smell to set/unset envvars in tests, as it causes races when run in parallel. Can you tell me more about what this is attempting to do? I might be able to suggest an alternative approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So essentially with new file commands, GitHub actions populate the environment with GITHUB_{CMD} variables, e.g. GITHUB_PATH with a file path where command data needs to be written. This is basically to test our code's behaviour end-to-end. We pass the same environment variable with a file path to it, that we expected from the GitHub runner. I originally didn't unset the env variables so when the tests ran in GitHub action environment, they failed because first part - of the tests - tests the fallback commands in absence of the variable. The issue obviously was that these variables were being populated by the runner.

Copy link
Owner

Choose a reason for hiding this comment

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

Could we just set GITHUB_PATH="" inside the action.yml instead?

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 think we can. Let me try!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sethvargo We're still stuck with 1 Setenv call per test for three test cases. The reason being that IssueFileCommand expects the file path to be passed as an environment variable, and AddPath and SetEnv are both dependent on it. Although resetting required env vars in workflow config does remove the reset/restore calls in tests, they still need to set for completing the tests.

Could you go through AddPath test again?

func TestAction_AddPath(t *testing.T) {
t.Parallel()
const envGitHubPath = "GITHUB_PATH"
defer os.Setenv(envGitHubPath, os.Getenv(envGitHubPath))
os.Unsetenv(envGitHubPath)
// expect a regular command to be issued when env file is not set.
var b bytes.Buffer
a := NewWithWriter(&b)
a.AddPath("/custom/bin")
if got, want := b.String(), "::add-path::/custom/bin\n"; got != want {
t.Errorf("expected %q to be %q", got, want)
}
// expect a file command to be issued when env file is set.
file, err := ioutil.TempFile(".", ".add_path_test_")
if err != nil {
t.Fatalf("unable to create a temp env file: %s", err)
}
defer os.Remove(file.Name())
if err = os.Setenv(envGitHubPath, file.Name()); err != nil {
t.Fatalf("unable to set %q env var: %s", envGitHubPath, err)
}
b.Reset()
a.AddPath("/custom/bin")
if got, want := b.String(), ""; got != want {
t.Errorf("expected %q to be %q", got, want)
}
// expect an empty stdout buffer
if got, want := b.String(), ""; got != want {
t.Errorf("expected %q to be %q", got, want)
}
// expect the message to be written to the file.
data, err := ioutil.ReadAll(file)
if err != nil {
t.Errorf("unable to read temp env file: %s", err)
}
if got, want := string(data), "/custom/bin\n"; got != want {
t.Errorf("expected %q to be %q", got, want)
}
}

In words, it does the following:

  1. Unset GITHUB_PATH (provided by the GitHub runner)
  2. Call AddPath() since GITHUB_PATH is unset, the test expects the fallback command to execute
  3. Create a temp file
  4. Set GITHUB_PATH to the temp file path
  5. Call AddPath() again and expect the file command to execute
  6. Verify if command data was written to the temp file
  7. File cleanup and env restoration

SetEnv test also follows the same pattern. Now even if we eliminate the Unset call, we still have to populate GITHUB_PATH with the temp file path.

Copy link
Owner

Choose a reason for hiding this comment

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

For testing, it's usually common to add another function like IssueFileCommandLookup that has the same API but accepts an interface that responds to what you want (Getenv in this case). That way, for tests, you can create a struct that implements that function on a map, and then IssueFileCommand becomes:

IssueFileCommandLookup(cmd, os.Getenv)

Does that make sense?

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 thought of that at some point. Lacked the confidence to carry it out in effect. lol

@ashutoshgngwr
Copy link
Contributor Author

@sethvargo On an unrelated note, for both Add-Path and Set-Env commands, the TypeScript toolkit ensures that the effects are visible to the current process immediately after execution. Should I go ahead and add it here as well?

https://github.com/actions/toolkit/blob/af821474235d3c5e1f49cee7c6cf636abb0874c4/packages/core/src/core.ts#L74
https://github.com/actions/toolkit/blob/af821474235d3c5e1f49cee7c6cf636abb0874c4/packages/core/src/core.ts#L43

@ashutoshgngwr
Copy link
Contributor Author

@sethvargo ping

@sethvargo
Copy link
Owner

Ugh sorry - I forgot to click "Submit Review" 😦

return c.issueFileCommand(cmd, os.Getenv)
}

func (c *Action) issueFileCommand(cmd *Command, f getenvFunc) error {
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 have created unexported functions in-place of suggested IssueFileCommandLookup. IMO, exporting these functions doesn't create any value. I've also found this pattern to be a common occurrence in Kubernetes code-base. Please let me know if you think that these should be exported functions instead.

Copy link
Owner

Choose a reason for hiding this comment

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

Unexported is fine. Less to worry about maintaining/breaking in the future 😄

return c.issueFileCommand(cmd, os.Getenv)
}

func (c *Action) issueFileCommand(cmd *Command, f getenvFunc) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Unexported is fine. Less to worry about maintaining/breaking in the future 😄

// newFakeGetenvFunc returns a new getenvFunc that is expected to be called with
// the provided key. It returns the provided value if the call matches the
// provided key. It reports an error on test t otherwise.
func newFakeGetenvFunc(t *testing.T, wantKey, v string) getenvFunc {
Copy link
Owner

Choose a reason for hiding this comment

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

Nit: you generally want to accept testing.TB instead of *testing.T so that the interface works with both benchmarks and tests.

@sethvargo sethvargo merged commit e057b58 into sethvargo:main Oct 20, 2020
@github-actions
Copy link

github-actions bot commented Nov 4, 2020

This pull request has been automatically locked since there has not
been any recent activity after it was closed. Please open a new
issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 4, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants