Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 1 addition & 4 deletions cmd/urga/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,7 @@ func open(cl cmdline) (ui.Model, error) {

// What the last session was looking at. A session that cannot be read
// starts fresh.
cfg, err := config.Load()
if err != nil {
return ui.Model{}, err
}
cfg := config.Load(filepath.Join(dir, "config.json"))

// Without clusters in the settings there is nothing to switch to.
var connect func(string) (ui.Connection, error)
Expand Down
16 changes: 16 additions & 0 deletions cmd/urga/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

tea "charm.land/bubbletea/v2"
"github.com/stretchr/testify/require"

"github.com/ingvarch/urga/internal/nomad"
Expand Down Expand Up @@ -257,6 +258,21 @@ address = "http://127.0.0.1:4646"
r.ErrorContains(err, `no cluster "stage": dev`)
}

func TestOpen_ReturnsToTheScreenOfTheSessionFile(t *testing.T) {
r := require.New(t)

home := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", home)
r.NoError(os.MkdirAll(filepath.Join(home, "urga"), 0o700))
r.NoError(os.WriteFile(filepath.Join(home, "urga", "config.json"), []byte(`{"screen": "nodes"}`), 0o600))

m, err := open(cmdline{})
r.NoError(err)

next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
r.Contains(next.View().Content, "Clients [")
}

func TestUpdateHint(t *testing.T) {
r := require.New(t)

Expand Down
37 changes: 7 additions & 30 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,29 +67,25 @@ func (c *Config) Of(cluster string) *Session {
return c.Clusters[cluster]
}

// Load reads the saved sessions. A first run, or a file that cannot be
// read, gives an empty config instead of an error that would stop urga.
func Load() (*Config, error) {
path, err := configPath()
if err != nil {
return nil, err
}

// Load reads the saved sessions from the file at path. A first run, or a
// file that cannot be read, gives an empty config instead of an error that
// would stop urga.
func Load(path string) *Config {
cfg := &Config{path: path}

data, err := os.ReadFile(path)
if err != nil {
return cfg, nil
return cfg
}

stored := &Config{}
if err := json.Unmarshal(data, stored); err != nil {
return cfg, nil
return cfg
}

stored.path = path

return stored, nil
return stored
}

// Snapshot takes the config as it is now. Take it where the config changes,
Expand Down Expand Up @@ -122,16 +118,6 @@ func (c *Config) Write(s Snapshot) error {
return nil
}

// Save writes the session down.
func (c *Config) Save() error {
s, err := c.Snapshot()
if err != nil {
return err
}

return c.Write(s)
}

// writeWhole replaces the file in one step, so a reader never sees it cut
// short.
func writeWhole(path string, data []byte) (err error) {
Expand Down Expand Up @@ -202,15 +188,6 @@ func Ordered(known, seen []string) []string {
return order
}

func configPath() (string, error) {
dir, err := Dir()
if err != nil {
return "", err
}

return filepath.Join(dir, "config.json"), nil
}

// Dir is where urga keeps what it remembers, and where the user writes its
// settings.
func Dir() (string, error) {
Expand Down
112 changes: 50 additions & 62 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,37 @@ import (
"github.com/ingvarch/urga/internal/config"
)

// write snapshots and writes, the way the UI does.
func write(cfg *config.Config) error {
s, err := cfg.Snapshot()
if err != nil {
return err
}

return cfg.Write(s)
}

func save(t *testing.T, cfg *config.Config) {
t.Helper()
require.NoError(t, write(cfg))
}

func TestConfig_RoundTrip(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", t.TempDir())
path := filepath.Join(t.TempDir(), "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)
r.Nil(cfg.Namespace)
r.Empty(cfg.Screen)

cfg.UseNamespace("production")
cfg.Screen = "deployments"
cfg.Namespaces = []string{"production", "staging"}

r.NoError(cfg.Save())
save(t, cfg)

again, err := config.Load()
r.NoError(err)
again := config.Load(path)

r.Equal("production", *again.Namespace)
r.Equal("deployments", again.Screen)
Expand All @@ -37,18 +50,16 @@ func TestConfig_RoundTrip(t *testing.T) {
func TestConfig_AllNamespacesSurvives(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", t.TempDir())
path := filepath.Join(t.TempDir(), "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

// Every namespace at once ("*") is saved like any other namespace. A
// session that ends on it starts on it again.
cfg.UseNamespace("*")
r.NoError(cfg.Save())
save(t, cfg)

again, err := config.Load()
r.NoError(err)
again := config.Load(path)

r.NotNil(again.Namespace)
r.Equal("*", *again.Namespace)
Expand All @@ -57,10 +68,9 @@ func TestConfig_AllNamespacesSurvives(t *testing.T) {
func TestConfig_UseNamespaceKeepsTheOrder(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", t.TempDir())
path := filepath.Join(t.TempDir(), "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

cfg.Remember([]string{"default", "production", "staging"})
cfg.Remember([]string{"staging", "default", "production"})
Expand All @@ -73,10 +83,9 @@ func TestConfig_UseNamespaceKeepsTheOrder(t *testing.T) {
func TestConfig_ANamespaceThatIsGoneLosesItsKey(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", t.TempDir())
path := filepath.Join(t.TempDir(), "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

cfg.Remember([]string{"default", "ml", "staging"})

Expand All @@ -93,47 +102,39 @@ func TestConfig_ANamespaceThatIsGoneLosesItsKey(t *testing.T) {
func TestConfig_MissingFileIsNoError(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "nothing", "here"))

cfg, err := config.Load()
cfg := config.Load(filepath.Join(t.TempDir(), "nothing", "here", "config.json"))

// A first run has nothing to read, and that is not a failure.
r.NoError(err)
r.NotNil(cfg)
}

func TestConfig_BrokenFileIsNoError(t *testing.T) {
r := require.New(t)

home := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", home)
path := filepath.Join(t.TempDir(), "config.json")

r.NoError(os.MkdirAll(filepath.Join(home, "urga"), 0o755))
r.NoError(os.WriteFile(filepath.Join(home, "urga", "config.json"), []byte("{not json"), 0o600))
r.NoError(os.WriteFile(path, []byte("{not json"), 0o600))

cfg, err := config.Load()
cfg := config.Load(path)

// A file that cannot be read starts the session fresh instead of
// stopping it.
r.NoError(err)
r.Nil(cfg.Namespace)
}

func TestConfig_ASessionForEachCluster(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", t.TempDir())
path := filepath.Join(t.TempDir(), "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

cfg.Of("prod").UseNamespace("batch")
cfg.Of("prod").Screen = "nodes"
cfg.Of("").UseNamespace("default")
r.NoError(cfg.Save())
save(t, cfg)

again, err := config.Load()
r.NoError(err)
again := config.Load(path)

// Each cluster starts again on the namespace and screen it was left on,
// and so does the cluster of the environment.
Expand All @@ -146,15 +147,12 @@ func TestConfig_ASessionForEachCluster(t *testing.T) {
func TestConfig_TheFileOfAnEarlierUrga(t *testing.T) {
r := require.New(t)

home := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", home)
path := filepath.Join(t.TempDir(), "config.json")

r.NoError(os.MkdirAll(filepath.Join(home, "urga"), 0o755))
r.NoError(os.WriteFile(filepath.Join(home, "urga", "config.json"),
r.NoError(os.WriteFile(path,
[]byte(`{"namespace": "staging", "screen": "jobs", "namespaces": ["default", "staging"]}`), 0o600))

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

// What an earlier urga wrote is read as the session of the environment.
session := cfg.Of("")
Expand All @@ -166,21 +164,17 @@ func TestConfig_TheFileOfAnEarlierUrga(t *testing.T) {
func TestConfig_SaveLeavesAWholeFileAndNothingElse(t *testing.T) {
r := require.New(t)

dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
folder := filepath.Join(t.TempDir(), "urga")
path := filepath.Join(folder, "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

cfg.UseNamespace("production")
r.NoError(cfg.Save())
save(t, cfg)

again, err := config.Load()
r.NoError(err)
again := config.Load(path)
r.Equal("production", *again.Namespace)

folder := filepath.Join(dir, "urga")

entries, err := os.ReadDir(folder)
r.NoError(err)
r.Len(entries, 1)
Expand All @@ -191,18 +185,14 @@ func TestConfig_SaveLeavesAWholeFileAndNothingElse(t *testing.T) {
func TestConfig_AFailedRenameLeavesNoTempFile(t *testing.T) {
r := require.New(t)

dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)

cfg, err := config.Load()
r.NoError(err)

folder := filepath.Join(dir, "urga")
folder := t.TempDir()
blocker := filepath.Join(folder, "config.json")
r.NoError(os.MkdirAll(filepath.Join(blocker, "inner"), 0o755))

cfg := config.Load(blocker)

cfg.UseNamespace("production")
r.Error(cfg.Save())
r.Error(write(cfg))

entries, err := os.ReadDir(folder)
r.NoError(err)
Expand All @@ -213,10 +203,9 @@ func TestConfig_AFailedRenameLeavesNoTempFile(t *testing.T) {
func TestConfig_AnEarlierSnapshotNeverOverwritesALaterOne(t *testing.T) {
r := require.New(t)

t.Setenv("XDG_CONFIG_HOME", t.TempDir())
path := filepath.Join(t.TempDir(), "config.json")

cfg, err := config.Load()
r.NoError(err)
cfg := config.Load(path)

cfg.UseNamespace("first")

Expand All @@ -231,7 +220,6 @@ func TestConfig_AnEarlierSnapshotNeverOverwritesALaterOne(t *testing.T) {
r.NoError(cfg.Write(second))
r.NoError(cfg.Write(first))

again, err := config.Load()
r.NoError(err)
again := config.Load(path)
r.Equal("second", *again.Namespace)
}
Loading
Loading