Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6d9ee74
Start writing 'vers run' integration test
jgoldstein46 Aug 22, 2025
f33038b
Add golden file for starting cluster
jgoldstein46 Aug 22, 2025
706bfee
Remove test vers.toml from .gitignore
jgoldstein46 Aug 22, 2025
7a289a2
Make fail to load test config cause fatal error
jgoldstein46 Aug 22, 2025
a5fd26b
Delete test utils
jgoldstein46 Aug 22, 2025
0197d49
Start writing integration test for vers run using os/exec package
jgoldstein46 Aug 25, 2025
80fdbd0
start constructing pipe for standard error in run test
jgoldstein46 Aug 25, 2025
ce0d8e4
Remove standard error piping
jgoldstein46 Aug 25, 2025
e440709
Complete start cluster test without vers init
jgoldstein46 Aug 25, 2025
25e9206
Add kill cluster for teardown
jgoldstein46 Aug 25, 2025
909079c
Clean up and add documentation
jgoldstein46 Aug 25, 2025
52d2c82
go fmt
AlephNotation Sep 2, 2025
7cf1262
remove go mod replace directive for forcing local cmd module usage
jgoldstein46 Sep 4, 2025
ff32ca3
Add function to check that environment variables have been set.
jgoldstein46 Sep 6, 2025
913839c
Add temporary vers install path and assign it to GOBIN
jgoldstein46 Sep 7, 2025
065e859
Add command to copy over test vers.toml into directory where vers run is
jgoldstein46 Sep 7, 2025
4d213c5
Extract creating the install directory to a separate function.
jgoldstein46 Sep 7, 2025
cf45c99
Update kill cluster command so that it will show an error message when
jgoldstein46 Sep 7, 2025
277296c
Update cluster ID extraction to be done in a separate function.
jgoldstein46 Sep 7, 2025
f720ccd
Remove extra parenthesis when getting VERS API Key from envrionment
jgoldstein46 Sep 7, 2025
837ff1f
Update exec command to check if stdout is not nil before logging output
jgoldstein46 Sep 7, 2025
0dd35ec
Delete unused TestStartCluster.golden file
jgoldstein46 Sep 7, 2025
9f500ed
Add documentation and clean up testing functions
jgoldstein46 Sep 7, 2025
a88a33c
Update README to exclude GO_INSTALL_PATH environment variable
jgoldstein46 Sep 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ vers.toml
.actrc
# Added by goreleaser init:
dist/

# Allow for testing
!test/testdata/vers.toml
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

A command-line interface for managing virtual machine/container-based development environments.


## Development

The scripts you should use as models are `status.go`, `execute.go`, `up.go`, `branch.go`.
The scripts you should use as models are `status.go`, `execute.go`, `up.go`, `branch.go`.

You can largely have AI generate new command scripts with those previous scripts as a model. You'll have to manually adjust the SDK calls, though, since the AI won't have access to the details of the SDK.

You can largely have AI generate new command scripts with those previous scripts as a model. You'll have to manually adjust the SDK calls, though, since the AI won't have access to the details of the SDK.
If a request specifies a parameter you'll see this type

If a request specifies a parameter you'll see this type
```
Command param.Field[string] `json:"command,required"`
```
Make sure that you prepare the parameter as follows: `vers.F(commandStr)`. See the "Request Fields" section of the [Go SDK Readme](https://github.com/hdresearch/vers-sdk-go) for more details. You can also look at the example of `execute.go`.

Make sure that you prepare the parameter as follows: `vers.F(commandStr)`. See the "Request Fields" section of the [Go SDK Readme](https://github.com/hdresearch/vers-sdk-go) for more details. You can also look at the example of `execute.go`.

## Features

Expand All @@ -29,7 +29,6 @@ Make sure that you prepare the parameter as follows: `vers.F(commandStr)`. See t
go install github.com/hdresearch/vers-cli/cmd/vers@latest
```


## Usage

### Available Commands
Expand All @@ -53,7 +52,7 @@ vers branch <vm-id>

## Configuration

Vers CLI uses a `vers.toml` configuration file to define your environment.
Vers CLI uses a `vers.toml` configuration file to define your environment.
The file should be created manually and can be customized for your specific needs.

Example:
Expand All @@ -74,20 +73,38 @@ command = "python main.py"
DATABASE_URL = "postgres://localhost:5432/mydb"
```


## Development

To build the binary locally, run:

```bash
go build -o bin/vers ./cmd/vers
```

This repository uses [Air](https://github.com/air-verse/air?tab=readme-ov-file) for development with hot reloading. You can run
This repository uses [Air](https://github.com/air-verse/air?tab=readme-ov-file) for development with hot reloading. You can run

```
air
```

which will take the place of running the binary. So to develop on e.g. `vers status` you would run

```
air status
```
```

### Integration Testing

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_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
```
200 changes: 200 additions & 0 deletions test/run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package test

import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"testing"

"os/exec"

"github.com/joho/godotenv"

"time"
)

func TestVersRun(t *testing.T) {
loadEnv(t)

checkEnv(t)

var installPath string = createTempInstallDir(t)

var versCliPath string = installVersCli(t, installPath)

login(t, versCliPath)

copyVersToml(t, installPath)

var got = execVersRun(t, versCliPath)

validateOutput(t, got)

clusterId, err := extractClusterId(got)

if err != nil {
t.Fatalf("Failed to extract cluster ID. Cannot kill cluster. Got error: %v", err)
}

killNewCluster(t, clusterId, versCliPath)
}

// Load VERS_API_KEY and GO_PATH
// The .env file should be located in the root directory of the project.
func loadEnv(t *testing.T) {
var rootDirPath string = getRootDirPath(t)
godotenv.Load(
filepath.Join(
rootDirPath,
".env",
),
)
}

// Validate that the necessary environment variables are set.
func checkEnv(t *testing.T) {
var goPath, versApiKey string = os.Getenv("GO_PATH"), os.Getenv("VERS_API_KEY")
var missing []string = []string{}

if goPath == "" {
missing = append(missing, "GO_PATH")
}
if versApiKey == "" {
missing = append(missing, "VERS_API_KEY")
}
if len(missing) > 0 {
t.Skipf("Skipping test because environment variables are not set. Missing: %v", missing)
}
}

// Create a temporary directory for installing the CLI.
// We also use this directory for the vers.toml
func createTempInstallDir(t *testing.T) string {
var currentDate string = time.Now().Format(time.DateTime)

tmpDir, err := filepath.Abs("/tmp")
if err != nil {
t.Fatal("Failed to resolve tmp directory. ")
}

installPath := filepath.Join(tmpDir, "vers-cli-run-test-"+currentDate)

execCommand(t, "", make(map[string]string), "mkdir", "-p", installPath)
return installPath
}

// Compile and install the local cmd/vers package
func installVersCli(t *testing.T, installPath string) string {
var goPath string = filepath.Clean(os.Getenv("GO_PATH"))
rootDirPath := getRootDirPath(t)

execCommand(
t, "", map[string]string{
"GOBIN": installPath,
}, goPath, "install", filepath.Join(rootDirPath, "cmd", "vers"),
)
return filepath.Join(installPath, "vers")
}

// Login to the locally installed CLI
func login(t *testing.T, versCliPath string) {

var apiKey string = os.Getenv("VERS_API_KEY")
execCommand(
t, "", make(map[string]string), versCliPath, "login", "--token", apiKey,
)
}

// Copy the test vers.toml to the testing directory
func copyVersToml(t *testing.T, installPath string) {
rootDirPath := getRootDirPath(t)
var versTomlPath string = filepath.Join(rootDirPath, "test", "testdata", "vers.toml")
var destPath string = filepath.Join(installPath, "vers.toml")

execCommand(
t, "", make(map[string]string), "cp", versTomlPath, destPath,
)
}

// Exec 'vers run'
func execVersRun(t *testing.T, versCliPath string) string {
return execCommand(
t, "./testdata", make(map[string]string), versCliPath, "run",
)
}

// Validate the output of 'vers run'
func validateOutput(t *testing.T, got string) {
var want = regexp.MustCompile(`Sending request to start cluster...\nCluster \(ID: \w+\) started successfully with root vm '[\w-]+'\.`)

if !want.MatchString(got) {
t.Errorf("Unexpected output. Want = %#q \nGot = %v", want, got)
}
}

// Extract the cluster ID
func extractClusterId(got string) (string, error) {
var re = regexp.MustCompile(`Cluster \(ID: (\w+)\) started successfully`)
var matches = re.FindStringSubmatch(got)

if len(matches) == 2 {
var clusterId = matches[1]
return clusterId, nil
}

return "", errors.New("Failed to extract cluster ID. No match found in output.")
}

// Teardown the newly created cluster
func killNewCluster(t *testing.T, clusterId string, versCliPath string) {
execCommand(t, "", make(map[string]string), versCliPath, "kill", "-c", "-y", clusterId)
}

// Utility functions

// Get the root directory path
func getRootDirPath(t *testing.T) string {
var rootDirPath string
var err error
rootDirPath, err = filepath.Abs("..")
if err != nil {
t.Fatal("Failed to resolve root directory. ")
}
return rootDirPath
}

// Executes commands in the specified directory
// Logs stdout and errors
// Command failures are fatal
func execCommand(t *testing.T, dir string, env map[string]string, command string, args ...string) string {

t.Logf("Executing command %v with args %v…\n", command, args)
var cmd = exec.Command(
command, args...,
)

// Set the command's directory
cmd.Dir = dir

// Update the command's environment variables
for k, v := range env {
cmd.Env = append(cmd.Environ(), fmt.Sprintf("%s=%s", k, v))
}

// Execute the command and capture the output
var output, err = cmd.Output()

var stdout string = ""
if output != nil {
stdout = string(output)
t.Logf("Got output: %v\n", stdout)
}

if err != nil {
t.Fatalf("Failed to execute command. Got error: %v\n", err)
}

return stdout
}
14 changes: 14 additions & 0 deletions test/testdata/vers.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[machine]
mem_size_mib = 512
vcpu_count = 1
fs_size_cluster_mib = 1024
fs_size_vm_mib = 512

[rootfs]
name = "default"

[builder]
name = "none"

[kernel]
name = "default.bin"