diff --git a/README.md b/README.md index de5ac5e..f935a20 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,13 @@ $ go work use . api git ### TODOs - Tests. + - tmux/state PruneSessions + - tmux/state MaybeFindRepository + - cmd new.go + - cmd rename.go + - cmd update.go no args + - cmd update.go with args + - cmd update.go completion suggestions - Update should accept repo-qualified work unit names. - tmux display-menu with better work unit ordering. - tmux hooks to automatically update session names when a session closes. diff --git a/api/repotest/repotest.go b/api/repotest/repotest.go index 211e2b3..d3ea1e5 100644 --- a/api/repotest/repotest.go +++ b/api/repotest/repotest.go @@ -9,14 +9,24 @@ import ( "github.com/JeffFaer/tmux-vcs-sync/api" ) -type fakeVCS struct{} +// NewVCS creates a new, fake VersionControlSystem that requires all +// repositories to be in the given directory. +func NewVCS(dir string) api.VersionControlSystem { + return fakeVCS{dir} +} -var VCS = fakeVCS{} +type fakeVCS struct { + dir string +} -func (fakeVCS) Name() string { return "fake" } +func (vcs fakeVCS) Name() string { return fmt.Sprintf("fake(%s)", vcs.dir) } func (fakeVCS) WorkUnitName() string { return "work unit" } -func (fakeVCS) Repository(dir string) (api.Repository, error) { +func (vcs fakeVCS) Repository(dir string) (api.Repository, error) { + if !strings.HasPrefix(dir, vcs.dir) { + return nil, nil + } return &fakeRepo{ + vcs: vcs, name: filepath.Base(dir), dir: dir, cur: "root", @@ -25,14 +35,15 @@ func (fakeVCS) Repository(dir string) (api.Repository, error) { } type fakeRepo struct { + vcs api.VersionControlSystem name, dir string cur string workUnits map[string]string } -func (*fakeRepo) VCS() api.VersionControlSystem { - return VCS +func (repo *fakeRepo) VCS() api.VersionControlSystem { + return repo.vcs } func (repo *fakeRepo) Name() string { diff --git a/api/repotest/repotest_test.go b/api/repotest/repotest_test.go index 51d15de..49ddc1b 100644 --- a/api/repotest/repotest_test.go +++ b/api/repotest/repotest_test.go @@ -1,7 +1,21 @@ package repotest -import "testing" +import ( + "path/filepath" + "strings" + "testing" + + "github.com/JeffFaer/tmux-vcs-sync/api" +) func TestFakeRepo(t *testing.T) { - RepoTests(t, VCS.Repository, Options{}) + pre := "testing/" + vcs := NewVCS(pre) + newRepo := func(dir string) (api.Repository, error) { + if !strings.HasPrefix(dir, pre) { + dir = filepath.Join("testing", dir) + } + return vcs.Repository(dir) + } + RepoTests(t, newRepo, Options{}) } diff --git a/tmux/server.go b/tmux/server.go index 2a25560..564fe15 100644 --- a/tmux/server.go +++ b/tmux/server.go @@ -174,13 +174,9 @@ func (srv *server) ListClients() ([]Client, error) { return res, nil } -func (srv *server) NewSession(opts ...NewSessionOption) (Session, error) { - opt := &newSessionOptions{} - for _, o := range opts { - o(opt) - } +func (srv *server) NewSession(opts NewSessionOptions) (Session, error) { args := []string{"new-session", "-d", "-P", "-F", string(SessionID)} - args = append(args, opt.args()...) + args = append(args, opts.args()...) newSession := srv.command(args...) newSession.Stdin = os.Stdin // tmux wants a tty. stdout, err := newSession.RunStdout() diff --git a/tmux/state/state.go b/tmux/state/state.go index 5cd3f6e..05f7366 100644 --- a/tmux/state/state.go +++ b/tmux/state/state.go @@ -23,6 +23,10 @@ type State struct { } func New(srv tmux.Server) (*State, error) { + return newState(srv, api.Registered) +} + +func newState(srv tmux.Server, vcs api.VersionControlSystems) (*State, error) { sessions, err := srv.ListSessions() if err != nil { return nil, err @@ -52,7 +56,7 @@ func New(srv tmux.Server) (*State, error) { repo, ok := reposByDir[path] if !ok { var err error - repo, err = api.Registered.MaybeFindRepository(path) + repo, err = vcs.MaybeFindRepository(path) if err != nil { logger.Warn("Error while checking for repository in tmux session.", "error", err) continue @@ -113,7 +117,7 @@ func (st *State) NewSession(repo api.Repository, workUnitName string) (tmux.Sess n := st.sessionNameString(name) slog.Info("Creating tmux session.", "name", name, "session_name", n) - sesh, err := st.srv.NewSession(tmux.NewSessionName(n), tmux.NewSessionStartDirectory(repo.RootDir())) + sesh, err := st.srv.NewSession(tmux.NewSessionOptions{Name: n, StartDir: repo.RootDir()}) if err != nil { return nil, fmt.Errorf("failed to create tmux session %q: %w", n, err) } diff --git a/tmux/state/state_test.go b/tmux/state/state_test.go new file mode 100644 index 0000000..346a355 --- /dev/null +++ b/tmux/state/state_test.go @@ -0,0 +1,608 @@ +package state + +import ( + stdcmp "cmp" + "fmt" + "testing" + + "github.com/JeffFaer/tmux-vcs-sync/api" + "github.com/JeffFaer/tmux-vcs-sync/api/repotest" + "github.com/JeffFaer/tmux-vcs-sync/tmux" + "github.com/JeffFaer/tmux-vcs-sync/tmux/tmuxtest" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestNew(t *testing.T) { + for _, tc := range []struct { + name string + + tmux tmux.Server + vcs api.VersionControlSystems + + want simplifiedState + }{ + { + name: "EmptyServer", + tmux: newServer(), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + want: simplifiedState{}, + }, + { + name: "SingleRepo", + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + tmux.NewSessionOptions{Name: "bar", StartDir: "testing/repo"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{{Repo: "repo"}}, + }, + }, + { + name: "UnknownSessions", + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + tmux.NewSessionOptions{Name: "bar", StartDir: "someOtherDir"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{{Repo: "repo"}}, + }, + }, + { + name: "MultipleRepos_UnqualifiedNames", + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo1"}, + tmux.NewSessionOptions{Name: "bar", StartDir: "testing/repo2"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo1"}, WorkUnit: "foo"}, + {RepoName: RepoName{Repo: "repo2"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo1", "repo2"}, + Repos: []RepoName{{Repo: "repo1"}, {Repo: "repo2"}}, + }, + }, + { + name: "MultipleRepos_QualifiedNames", + tmux: newServer( + tmux.NewSessionOptions{Name: "repo1>foo", StartDir: "testing/repo1"}, + tmux.NewSessionOptions{Name: "repo2>bar", StartDir: "testing/repo2"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo1"}, WorkUnit: "foo"}, + {RepoName: RepoName{Repo: "repo2"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo1", "repo2"}, + Repos: []RepoName{{Repo: "repo1"}, {Repo: "repo2"}}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + st, err := newState(tc.tmux, tc.vcs) + if err != nil { + t.Errorf("newState() = _, %v", err) + } + if diff := cmp.Diff(tc.want, simplifyState(st), compareSimplifiedStates, cmpopts.IgnoreFields(RepoName{}, "VCS")); diff != "" { + t.Errorf("State diff (-want +got)\n%s", diff) + } + }) + } +} + +func TestNewSession(t *testing.T) { + for _, tc := range []struct { + name string + + tmux tmux.Server + vcs api.VersionControlSystems + + repoDir, workUnitName string + + want simplifiedState + wantTmux simplifiedTmuxState + wantErr bool + }{ + { + name: "Empty", + + tmux: newServer(), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo", + workUnitName: "foo", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{ + {Repo: "repo"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "foo", + Dir: "testing/repo", + }, + }, + }, + }, + { + name: "SingleRepo", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo", + workUnitName: "bar", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{ + {Repo: "repo"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "foo", + Dir: "testing/repo", + }, + { + Name: "bar", + Dir: "testing/repo", + }, + }, + }, + }, + { + name: "MultipleRepos", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo1"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo2", + workUnitName: "bar", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo1"}, WorkUnit: "foo"}, + {RepoName: RepoName{Repo: "repo2"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo1", "repo2"}, + Repos: []RepoName{ + {Repo: "repo1"}, + {Repo: "repo2"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "repo1>foo", + Dir: "testing/repo1", + }, + { + Name: "repo2>bar", + Dir: "testing/repo2", + }, + }, + }, + }, + { + name: "SessionAlreadyExists_Error", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo", + workUnitName: "foo", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{ + {Repo: "repo"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "foo", + Dir: "testing/repo", + }, + }, + }, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + st, err := newState(tc.tmux, tc.vcs) + if err != nil { + t.Fatalf("newState() = _, %v", err) + } + repo, err := tc.vcs.MaybeFindRepository(tc.repoDir) + if err != nil { + t.Fatalf("MaybeFindRepository(%q) = _, %v", tc.repoDir, err) + } + if repo == nil { + t.Fatalf("tc.repoDir did not yield a repository") + } + + if _, err := st.NewSession(repo, tc.workUnitName); (err != nil) != tc.wantErr { + t.Errorf("NewSession(%q, %q) = %v, wantErr %t", tc.repoDir, tc.workUnitName, err, tc.wantErr) + } + + if diff := cmp.Diff(tc.want, simplifyState(st), compareSimplifiedStates, cmpopts.IgnoreFields(RepoName{}, "VCS")); diff != "" { + t.Errorf("State diff (-want +got)\n%s", diff) + } + if diff := cmp.Diff(tc.wantTmux, simplifyTmuxState(tc.tmux), compareSimplifiedTmuxState); diff != "" { + t.Errorf("tmux diff (-want +got)\n%s", diff) + } + }) + } +} + +func TestRename(t *testing.T) { + for _, tc := range []struct { + name string + + tmux tmux.Server + vcs api.VersionControlSystems + + repoDir, old, new string + + want simplifiedState + wantTmux simplifiedTmuxState + wantErr bool + }{ + { + name: "Simple", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo", + old: "foo", + new: "bar", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{ + {Repo: "repo"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "bar", + Dir: "testing/repo", + }, + }, + }, + }, + { + name: "MultipleRepos_UnqualifiedNames", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo1"}, + tmux.NewSessionOptions{Name: "bar", StartDir: "testing/repo2"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo1", + old: "foo", + new: "baz", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo1"}, WorkUnit: "baz"}, + {RepoName: RepoName{Repo: "repo2"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo1", "repo2"}, + Repos: []RepoName{ + {Repo: "repo1"}, + {Repo: "repo2"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "repo1>baz", + Dir: "testing/repo1", + }, + { + Name: "repo2>bar", + Dir: "testing/repo2", + }, + }, + }, + }, + { + name: "MultipleRepos_QualifiedNames", + + tmux: newServer( + tmux.NewSessionOptions{Name: "repo1>foo", StartDir: "testing/repo1"}, + tmux.NewSessionOptions{Name: "repo2>bar", StartDir: "testing/repo2"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo1", + old: "repo1>foo", + new: "baz", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo1"}, WorkUnit: "baz"}, + {RepoName: RepoName{Repo: "repo2"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo1", "repo2"}, + Repos: []RepoName{ + {Repo: "repo1"}, + {Repo: "repo2"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "repo1>baz", + Dir: "testing/repo1", + }, + { + Name: "repo2>bar", + Dir: "testing/repo2", + }, + }, + }, + }, + { + name: "OldDoesNotExist_Error", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo", + old: "bar", + new: "foo", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{ + {Repo: "repo"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "foo", + Dir: "testing/repo", + }, + }, + }, + wantErr: true, + }, + { + name: "NewAlreadyExists_Error", + + tmux: newServer( + tmux.NewSessionOptions{Name: "foo", StartDir: "testing/repo"}, + tmux.NewSessionOptions{Name: "bar", StartDir: "testing/repo"}, + ), + vcs: api.VersionControlSystems{ + repotest.NewVCS("testing/"), + }, + + repoDir: "testing/repo", + old: "foo", + new: "bar", + + want: simplifiedState{ + Sessions: []SessionName{ + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "foo"}, + {RepoName: RepoName{Repo: "repo"}, WorkUnit: "bar"}, + }, + UnqualifiedRepos: []string{"repo"}, + Repos: []RepoName{ + {Repo: "repo"}, + }, + }, + wantTmux: simplifiedTmuxState{ + Sessions: []simplifiedSessionState{ + { + Name: "foo", + Dir: "testing/repo", + }, + { + Name: "bar", + Dir: "testing/repo", + }, + }, + }, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + st, err := newState(tc.tmux, tc.vcs) + if err != nil { + t.Fatalf("newState() = _, %v", err) + } + repo, err := tc.vcs.MaybeFindRepository(tc.repoDir) + if err != nil { + t.Fatalf("MaybeFindRepository(%q) = _, %v", tc.repoDir, err) + } + if repo == nil { + t.Fatalf("tc.repoDir did not yield a repository") + } + + if err := st.RenameSession(repo, tc.old, tc.new); (err != nil) != tc.wantErr { + t.Errorf("RenameSession(%q, %q, %q) = %v, wantErr = %t", tc.repoDir, tc.old, tc.new, err, tc.wantErr) + } + + if diff := cmp.Diff(tc.want, simplifyState(st), compareSimplifiedStates, cmpopts.IgnoreFields(RepoName{}, "VCS")); diff != "" { + t.Errorf("State diff (-want +got)\n%s", diff) + } + if diff := cmp.Diff(tc.wantTmux, simplifyTmuxState(tc.tmux), compareSimplifiedTmuxState); diff != "" { + t.Errorf("tmux diff (-want +got)\n%s", diff) + } + }) + } +} + +type simplifiedState struct { + Sessions []SessionName + UnqualifiedRepos []string + Repos []RepoName +} + +var compareSimplifiedStates = cmp.Options{ + cmpopts.SortSlices(func(a, b SessionName) bool { + if a.VCS != b.VCS { + return a.VCS < b.VCS + } + if a.Repo != b.Repo { + return a.Repo < b.Repo + } + return a.WorkUnit < b.WorkUnit + }), + cmpopts.SortSlices(stdcmp.Less[string]), + cmpopts.SortSlices(func(a, b RepoName) bool { + if a.VCS != b.VCS { + return a.VCS < b.VCS + } + return a.Repo < b.Repo + }), +} + +func simplifyState(st *State) simplifiedState { + var ret simplifiedState + for n := range st.sessions { + ret.Sessions = append(ret.Sessions, n) + } + for n := range st.unqualifiedRepos { + ret.UnqualifiedRepos = append(ret.UnqualifiedRepos, n) + } + for n := range st.repos { + ret.Repos = append(ret.Repos, n) + } + return ret +} + +type simplifiedTmuxState struct { + Sessions []simplifiedSessionState +} + +type simplifiedSessionState struct { + ID string + Name string + Dir string +} + +var compareSimplifiedTmuxState = cmp.Options{ + cmpopts.IgnoreFields(simplifiedSessionState{}, "ID"), + cmpopts.SortSlices(func(a, b simplifiedSessionState) bool { + return a.ID < b.ID + }), +} + +func simplifyTmuxState(srv tmux.Server) simplifiedTmuxState { + var ret simplifiedTmuxState + for _, sesh := range must(srv.ListSessions()) { + props := must(sesh.Properties(tmux.SessionName, tmux.SessionPath)) + ret.Sessions = append(ret.Sessions, simplifiedSessionState{ + ID: sesh.ID(), + Name: props[tmux.SessionName], + Dir: props[tmux.SessionPath], + }) + } + return ret +} + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +var pid = 1 + +func newServer(opts ...tmux.NewSessionOptions) tmux.Server { + srv := tmuxtest.NewServer(pid) + pid++ + for _, opts := range opts { + _, err := srv.NewSession(opts) + if err != nil { + panic(fmt.Errorf("srv.NewSession(%#v) = %w", opts, err)) + } + } + return srv +} diff --git a/tmux/tmux.go b/tmux/tmux.go index 9ab6e8a..76f650f 100644 --- a/tmux/tmux.go +++ b/tmux/tmux.go @@ -30,7 +30,7 @@ type Server interface { ListClients() ([]Client, error) // NewSession creates a new session in this tmux server. - NewSession(opts ...NewSessionOption) (Session, error) + NewSession(NewSessionOptions) (Session, error) // AttachOrSwitch either attaches the controlling terminal to the given TargetSession or switches the current tmux client to the TargetSession. AttachOrSwitch(Session) error @@ -38,39 +38,25 @@ type Server interface { Kill() error } -// NewSessionOption affects how NewSession creates sessions. -type NewSessionOption func(*newSessionOptions) - -type newSessionOptions struct { - name string - startDir string +// NewSessionOptions affects how NewSession creates sessions. +type NewSessionOptions struct { + // Name is the optional initial name for the session. + Name string + // StartDir is the optional initial working directory for the session. + StartDir string } -func (opts newSessionOptions) args() []string { +func (opts NewSessionOptions) args() []string { var res []string - if opts.name != "" { - res = append(res, []string{"-s", opts.name}...) + if opts.Name != "" { + res = append(res, []string{"-s", opts.Name}...) } - if opts.startDir != "" { - res = append(res, []string{"-c", opts.startDir}...) + if opts.StartDir != "" { + res = append(res, []string{"-c", opts.StartDir}...) } return res } -// NewSessionName creates the new session with the given initial name. -func NewSessionName(name string) NewSessionOption { - return func(opts *newSessionOptions) { - opts.name = name - } -} - -// NewSessionStartDirectory creates the new session with the given initial start directory. -func NewSessionStartDirectory(dir string) NewSessionOption { - return func(opts *newSessionOptions) { - opts.startDir = dir - } -} - type Session interface { // Server returns the tmux server this Session belongs to. Server() Server diff --git a/tmux/tmux_test.go b/tmux/tmux_test.go index 624178b..40308c4 100644 --- a/tmux/tmux_test.go +++ b/tmux/tmux_test.go @@ -62,8 +62,8 @@ func randomString() string { return base58.Encode(b) } -func (srv TestServer) MustNewSession(opts ...NewSessionOption) TestSession { - sesh, err := srv.NewSession(opts...) +func (srv TestServer) MustNewSession(opts NewSessionOptions) TestSession { + sesh, err := srv.NewSession(opts) if err != nil { srv.t.Fatal(err) } @@ -175,9 +175,9 @@ func TestServer_Sessions(t *testing.T) { t.Errorf("New tmux server has %d sessions, expected 0", len(sessions)) } - a := srv.MustNewSession(NewSessionName("a")) - b := srv.MustNewSession(NewSessionName("b")) - c := srv.MustNewSession(NewSessionName("c")) + a := srv.MustNewSession(NewSessionOptions{Name: "a"}) + b := srv.MustNewSession(NewSessionOptions{Name: "b"}) + c := srv.MustNewSession(NewSessionOptions{Name: "c"}) sessions = srv.MustListSessions() if diff := cmp.Diff([]TestSession{a, b, c}, sessions, tmuxCmpOpt); diff != "" { @@ -194,8 +194,8 @@ func TestServer_Sessions(t *testing.T) { func TestServer_AttachOrSwitch(t *testing.T) { srv := NewServerForTesting(t) - a := srv.MustNewSession(NewSessionName("a")) - b := srv.MustNewSession(NewSessionName("b")) + a := srv.MustNewSession(NewSessionOptions{Name: "a"}) + b := srv.MustNewSession(NewSessionOptions{Name: "b"}) if c := srv.MustListClients(); len(c) != 0 { t.Errorf("Server already has %d clients:\n%v", len(c), c) diff --git a/tmux/tmuxtest/tmuxtest.go b/tmux/tmuxtest/tmuxtest.go new file mode 100644 index 0000000..8861ce4 --- /dev/null +++ b/tmux/tmuxtest/tmuxtest.go @@ -0,0 +1,156 @@ +package tmuxtest + +import ( + "fmt" + "os" + "strconv" + + "github.com/JeffFaer/tmux-vcs-sync/tmux" +) + +type Server struct { + pid int + + nextSessionID int + sessions map[string]*Session + + CurrentSession *Session +} + +var _ tmux.Server = (*Server)(nil) + +// In the real world, different tmux.Server instances would return the same +// state since they're calling out to real tmux. Simulate that by caching +// instances per pid. +var servers = make(map[int]*Server) + +func NewServer(pid int) *Server { + if srv := servers[pid]; srv != nil { + return srv + } + servers[pid] = &Server{pid: pid} + return servers[pid] +} + +func (srv *Server) PID() (int, error) { return srv.pid, nil } + +func (srv *Server) ListSessions() ([]tmux.Session, error) { + var ret []tmux.Session + for _, sesh := range srv.sessions { + if sesh.dead { + continue + } + ret = append(ret, sesh) + } + return ret, nil +} + +func (srv *Server) ListClients() ([]tmux.Client, error) { + return nil, nil +} + +func (srv *Server) NewSession(opts tmux.NewSessionOptions) (tmux.Session, error) { + idNum := srv.nextSessionID + id := fmt.Sprintf("%d#%d", srv.pid, idNum) + srv.nextSessionID++ + + name := strconv.Itoa(idNum) + if n := opts.Name; n != "" { + name = n + } + + dir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("Getwd: %w", err) + } + if d := opts.StartDir; d != "" { + dir = d + } + + if srv.sessions == nil { + srv.sessions = make(map[string]*Session) + } + srv.sessions[id] = &Session{ + srv: srv, + id: id, + props: map[tmux.SessionProperty]string{ + tmux.SessionID: id, + tmux.SessionName: name, + tmux.SessionPath: dir, + }, + } + return srv.sessions[id], nil +} + +func (srv *Server) AttachOrSwitch(sesh tmux.Session) error { + if !tmux.SameServer(srv, sesh.Server()) { + return fmt.Errorf("session %q does not belong to this server", sesh.ID()) + } + if srv.sessions[sesh.ID()].dead { + return fmt.Errorf("session %q was killed", sesh.ID()) + } + srv.CurrentSession = srv.sessions[sesh.ID()] + return nil +} + +func (srv *Server) Kill() error { + srv.sessions = nil + srv.CurrentSession = nil + return nil +} + +type Session struct { + srv *Server + id string + + props map[tmux.SessionProperty]string + dead bool +} + +var _ tmux.Session = (*Session)(nil) + +func (s *Session) Server() tmux.Server { return s.srv } +func (s *Session) ID() string { return s.id } + +func (s *Session) Property(prop tmux.SessionProperty) (string, error) { + vals, err := s.Properties(prop) + if err != nil { + return "", err + } + return vals[prop], nil +} + +func (s *Session) Properties(props ...tmux.SessionProperty) (map[tmux.SessionProperty]string, error) { + if s.dead { + return nil, fmt.Errorf("session %q was killed", s.id) + } + + ret := make(map[tmux.SessionProperty]string) + for _, prop := range props { + ret[prop] = s.props[prop] + } + return ret, nil +} + +func (s *Session) setProperty(k tmux.SessionProperty, v string) { + if s.props == nil { + s.props = make(map[tmux.SessionProperty]string) + } + s.props[k] = v +} + +func (s *Session) Rename(n string) error { + if s.dead { + return fmt.Errorf("session %q was killed", s.id) + } + s.setProperty(tmux.SessionName, n) + return nil +} + +func (s *Session) Kill() error { + if s.dead { + return fmt.Errorf("session %q was already killed", s.id) + } + s.dead = true + return nil +}