Conversation
| Add environment the following environment variables to a .env file at the root of this repository. | ||
|
|
||
| ```bash | ||
| VERS_URL="https://api.vers.sh" | ||
| VERS_API_KEY="sk-vers-api…" | ||
| GO_INSTALL_PATH=… # Where go installs your modules. Likely "$HOME/go/bin/" | ||
| GO_PATH=… # Path to your go executable. E.g. "/usr/local/go/bin/go" | ||
| ``` | ||
|
|
||
| To run the integration tests in the `test` package, run | ||
|
|
||
| ```bash | ||
| cd test && go test -v | ||
| ``` |
There was a problem hiding this comment.
Just to confirm: we want a .env in the project root, even though we're cd'ing into the test directory? (See later comments re: setting up a temp directory.)
There was a problem hiding this comment.
Yes! I noted this above the .env
test/run_test.go
Outdated
| fmt.Sprintf( | ||
| "%v/.env", | ||
| rootDirPath, |
There was a problem hiding this comment.
nit: Generally, prefer path resolution libraries over generating arbitrary path strings. It's more explicit, handles common errors like having double forward slashes, and handles this behavior in a platform-independent manner (escaping characters that may need escaping, using backslashes on Windows, etc.)
https://pkg.go.dev/path
There was a problem hiding this comment.
Thanks for mentioning this! I believe the filepath package was what you meant -- this is a better fit for handling os paths. Updated all references path references to use this.
test/run_test.go
Outdated
|
|
||
| // Compile and install the local cmd/vers package | ||
| func installVersCli(t *testing.T) { | ||
| var goPath string = os.Getenv("GO_PATH") |
There was a problem hiding this comment.
Assumes variable is set and doesn't provide feedback if not. (Incidentally, this variable being unset is why the test on GH is currently failing.) Maybe it should attempt to exec go without a fully qualified path if this isn't set? Or maybe it should just throw a more helpful error.
There was a problem hiding this comment.
Added a check for all environment variables being set. If not all are set, the test will skip and log which are missing.
There was a problem hiding this comment.
Execing go without a fully qualified path has not been working for me because of the way the exec package works. I believe it expects a full path to your go executable -- it's not like a shell with a PATH environment variable.
| execCommand( | ||
| t, "", goPath, "install", fmt.Sprintf("%v/cmd/vers", rootDirPath), | ||
| ) |
There was a problem hiding this comment.
Note that using go install will install it to the user's Go bin path, usually at ~/go/bin. Recommend creating a temporary install directory (In /tmp maybe?) and installing there.
mkdir -p /tmp/vers-cli-test-abc123
GOBIN=/tmp/vers-cli-test-abc123 go install $PROJECT_ROOT/cmd/versRemember then to use the temp directory as the path to the vers binary. This eliminates the need for the GO_INSTALL_PATH var, since you can use a return value from your createTempInstallDirectory() function. You can additionally copy the vers.toml and any necessary stuff here, this way we can isolate the test environment, rather than mutating or potentially relying on something in the host environment.
There was a problem hiding this comment.
Good idea, I updated the test to create the temp install directory, and move the test vers.toml there as well.
test/run_test.go
Outdated
| var got = execVersRun(t) | ||
|
|
||
| validateOutput(t, got) |
There was a problem hiding this comment.
If the test fails immediately due to a mismatch in expected output (rather than due to an actual error), the new cluster will not be cleaned up properly.
There was a problem hiding this comment.
I was using testing.Errorf() for validating the output, which causes the test to fail, but allows execution to continue. To make it obvious if there was an issue killing the cluster, I just added another error logging statement so that whoever is running the test will know!
test/run_test.go
Outdated
|
|
||
| validateOutput(t, got) | ||
|
|
||
| killNewCluster(t, got) |
There was a problem hiding this comment.
As a nitpick, I'd rather see a cluster ID passed into this function. validateOutput, for example, could return an extracted cluster ID since it's already doing a regexp validation.
There was a problem hiding this comment.
I think that ID extraction should be done in a separate function, since it uses a slightly looser regex to extract the ID. I moved it to a separate function, and now pass in the extracted ID to the kill cluster command. This separate function + logging also makes it more obvious when the cluster ID cannot be extracted.
test/run_test.go
Outdated
| func login(t *testing.T) { | ||
| cliPath := getVersCliPath() | ||
|
|
||
| var apiKey string = os.Getenv(("VERS_API_KEY")) |
test/run_test.go
Outdated
|
|
||
| var stdout = string(output) | ||
|
|
||
| t.Logf("Got output: %v\n", stdout) |
There was a problem hiding this comment.
Question: Is stdout defined if err is non-nil? I'm very used to seeing immediate error checks in Go, since I tend to assume that the first value may be nil if the error is non-nil, which will cause a segfault due to uninitialized/nil pointer dereference.
There was a problem hiding this comment.
Looking at the source code, cmd.Output() will return stdout even if stderr is not nil. I added a check in the case that standard out is already defined and cmd.Output() exits early -- in which case it will return nil for standard out and we want to avoid the segfault.
// Output runs the command and returns its standard output.
// Any returned error will usually be of type [*ExitError].
// If c.Stderr was nil and the returned error is of type
// [*ExitError], Output populates the Stderr field of the
// returned error.
func (c *Cmd) Output() ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
var stdout bytes.Buffer
c.Stdout = &stdout
captureErr := c.Stderr == nil
if captureErr {
c.Stderr = &prefixSuffixSaver{N: 32 << 10}
}
err := c.Run()
if err != nil && captureErr {
if ee, ok := err.(*ExitError); ok {
ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes()
}
}
return stdout.Bytes(), err
}| Sending request to start cluster... | ||
| Cluster (ID: cluster-\w+) started successfully with root vm 'vm-\w+'. | ||
| HEAD now points to: vm-\w+ No newline at end of file |
There was a problem hiding this comment.
What an interesting file name hahaha! Afaict this is unused though. What format is this? It's like. Almost some kind of regexp, but not, because it contains unescaped periods.
There was a problem hiding this comment.
Oh haha, it's plaintext. Yeah not being used because this package was a bit too brittle in that it did not allow me to compare output via regex. Removing this file for now. If you're interested in the package, you can check it out here.
Ty's friend who's building the crush cli was using it so I started to try it.
This directive was not necessary since go looks for modules locally before fetching them over the internet
If environment variables are not set, the function logs which ones are missing, then skips the rest of the test.
This isolates the testing environment, and makes it so that the GO_INSTALL_PATH environment variable is no longer needed.
This creates consistency in that each function is responsible for one task at a time.
the cluster is still alive if killing it fails.
This isolates functions' concerns and shows clearly when the ID cannot be extracted.
This avoids a segfault in case there is no output returned by the cmd.Output() execution
There was a problem hiding this comment.
Looks good! I only have three notes, but none of these are worth blocking the PR for:
- We may want to attempt to clean up the temp dir after tests are run if a flag is/isn't set. Not crucial, since temp directories should self-clean.
- Anyone running this against a dev node rather than a load balancer will have the login command fail. This seems reasonable, since the integration test might want to verify that login works. But it's just a case of like. "If you are running this against a dev node, you'll want to comment out the
login()invocation, because it will fail." Chelsea does not expose the verify API token endpoint that it uses, while the LB does. - Having to pass in the go path is a little odd; it might be the case that GO_PATH should be optional. Usually when you exec, you can let the user's PATH variable handle the heavy lifting:
go test -vwill try all entries in PATH until it findsgoin one of them. In fact, the way I run the tests currently isGO_PATH=$(which go) go test -v, sincewhichwill return the same path that my terminal finds.
Minimal example:
// ./test1/main.go
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
var cmd = exec.Command("go", "run", "../test2/main.go")
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Printf("Error: %s\n", exitErr.Stderr)
os.Exit(exitErr.ExitCode())
} else {
fmt.Printf("Unrecognized error: %v\n", err)
os.Exit(1)
}
}
fmt.Printf("Success! Stdout: %s", output)
}// ./test2/main.go
package main
import "fmt"
func main() {
fmt.Printf("This is another main function!\n")
}Output:
Success! Stdout: This is another main function!
Note that it's not necessary to specify the Go path. The exec.Command call spawns the command with the user's shell, which in my case is zsh. Zsh sources my .zshrc file, yadda yadda, basically my PATH variable contains /usr/local/go/bin, and that directory has the go executable, so my shell can resolve the command. GO_PATH may be optional if my shell doesn't have the path, but otherwise you can try to take advantage of it.
Super minor, if you wanna tweak it for that last bullet point feel free to, but yeah. Everything else looks good! The temp directory stuff is clean and works as expected. :)
vers runcommand.cd test && go test -v-> Verify cluster started and torn down.