From 7d9024ac04614e3116f685952d5aa2ab344ba531 Mon Sep 17 00:00:00 2001 From: "Mike JS. Choi" Date: Mon, 21 May 2018 15:37:16 -0500 Subject: [PATCH] Update editor reading/writing mechanism --- .travis.yml | 2 +- editor/content.go | 40 ++++++++++++++++++++++++++-------------- editor/content_test.go | 9 ++++----- editor/editor.go | 29 +++++++++++------------------ editor/editor_test.go | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 38 deletions(-) diff --git a/.travis.yml b/.travis.yml index 177c06c..7f070e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ before_install: - go get -t -v ./... script: - - go test -race -coverprofile=coverage.txt -covermode=atomic `go list ./...` + - go test -race -coverprofile=coverage.txt -covermode=atomic `go list ./... | grep -v testhelper` after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/editor/content.go b/editor/content.go index 9c8face..e97a2d8 100755 --- a/editor/content.go +++ b/editor/content.go @@ -7,7 +7,9 @@ import ( "strings" ) -// Content holds all I/O related logic of the editor +// Content represents the lines read from a io.Reader +// `c` holds the actual lines and +// `reader` contains the io.Reader representation of the lines type Content struct { c []string reader io.Reader @@ -18,30 +20,40 @@ func (c Content) Read(b []byte) (int, error) { } func (c Content) String() string { - return strings.Join(c.c, "\n") + return strings.Join(c.c, "") } // contentFromReader creates a new `Content` object -// by scanning an io.Reader using a bufio.SplitFunc -func contentFromReader(content io.Reader, split bufio.SplitFunc) (Content, error) { - c := Content{} - scanner := bufio.NewScanner(content) - scanner.Split(split) - - for scanner.Scan() { - c.c = append(c.c, scanner.Text()) +// filled with the lines from the provided Reader +// It returns an error if anything other than io.EOF is raised +func contentFromReader(content io.Reader) (c Content, err error) { + reader := bufio.NewReader(content) + + for { + line, err := reader.ReadString('\n') + c.c = append(c.c, line) + + if err != nil { + break + } + } + + if err != nil && err != io.EOF { + return } - c.reader = strings.NewReader(c.String()) - return c, scanner.Err() + c.reader = strings.NewReader(c.String()) + return } -func contentFromFile(filename string, split bufio.SplitFunc) (Content, error) { +// contentFromFile reads the content from the file +// It returns an error if the file does not exist +func contentFromFile(filename string) (Content, error) { file, err := os.Open(filename) if err != nil { return Content{}, err } defer file.Close() - return contentFromReader(file, split) + return contentFromReader(file) } diff --git a/editor/content_test.go b/editor/content_test.go index 8491c8d..b2de76e 100644 --- a/editor/content_test.go +++ b/editor/content_test.go @@ -1,7 +1,6 @@ package editor import ( - "bufio" "io/ioutil" "strings" "testing" @@ -10,7 +9,7 @@ import ( ) func TestUseContentAsReader(t *testing.T) { - c, err := contentFromReader(strings.NewReader("foo\nbar"), bufio.ScanLines) + c, err := contentFromReader(strings.NewReader("foo\nbar")) testhelper.Ok(t, err) byteCnt, err := ioutil.ReadAll(c) @@ -24,8 +23,8 @@ func TestCreateContentFromFile(t *testing.T) { _, err = f.Write([]byte("foo\nbar")) testhelper.Ok(t, err) - c, err := contentFromFile(f.Name(), bufio.ScanLines) + c, err := contentFromFile(f.Name()) - testhelper.Equals(t, c.c[0], "foo") - testhelper.Equals(t, c.c[1], "bar") + testhelper.Equals(t, "foo\n", c.c[0]) + testhelper.Equals(t, "bar", c.c[1]) } diff --git a/editor/editor.go b/editor/editor.go index 0ddca9c..44a6522 100755 --- a/editor/editor.go +++ b/editor/editor.go @@ -4,7 +4,6 @@ package editor // https://github.com/kioopi/extedit import ( - "bufio" "io" "io/ioutil" "os" @@ -16,19 +15,16 @@ import ( const defaultEditor = "vim" -type Session struct { - input Content - SplitFunc bufio.SplitFunc -} +var execCommand = exec.Command -// Open starts a text-editor with the contents of content. -// It returns edited content after user closes the editor +// Open starts a text-editor with lines from `Content` +// It returns the manually edited lines from the text-editor when the user closes the editor func Open(c *conflict.Conflict) (output []string, err error) { - s := &Session{SplitFunc: bufio.ScanLines} lines := c.File.Lines[c.Start : c.End+1] content := strings.NewReader(strings.Join(lines, "")) - input, err := contentFromReader(content, s.SplitFunc) + input, err := contentFromReader(content) + if err != nil { return } @@ -44,20 +40,17 @@ func Open(c *conflict.Conflict) (output []string, err error) { return } - newContent, err := contentFromFile(fileName, s.SplitFunc) + newContent, err := contentFromFile(fileName) if err != nil { return } output = newContent.c - for i := range output { - output[i] = output[i] + "\n" - } return } -// writeTmpFile writes content to a temporary file and returns -// the path to the file +// writeTmpFile writes content to a temporary file and returns the path to the file +// It returns an error if the temporary file cannot be created func writeTmpFile(content io.Reader) (string, error) { f, err := ioutil.TempFile("", "") if err != nil { @@ -69,14 +62,14 @@ func writeTmpFile(content io.Reader) (string, error) { return f.Name(), nil } -// editorCmd creates a os/exec.Cmd to open -// filename in an editor ready to be run() +// editorCmd returns a os/exec.Cmd to open the provided file func editorCmd(filename string) *exec.Cmd { editorPath := os.Getenv("EDITOR") if editorPath == "" { editorPath = defaultEditor } - editor := exec.Command(editorPath, filename) + + editor := execCommand(editorPath, filename) editor.Stdin = os.Stdin editor.Stdout = os.Stdout diff --git a/editor/editor_test.go b/editor/editor_test.go index 541940b..6d8b809 100644 --- a/editor/editor_test.go +++ b/editor/editor_test.go @@ -1,9 +1,12 @@ package editor import ( + "os" + "os/exec" "strings" "testing" + "github.com/mkchoi212/fac/conflict" "github.com/mkchoi212/fac/testhelper" ) @@ -19,3 +22,36 @@ func TestWriteTmpFile(t *testing.T) { testhelper.Ok(t, err) testhelper.Assert(t, name != "", "tmp file name should not be empty") } + +func TestOpen(t *testing.T) { + execCommand = mockExecCommand + defer func() { execCommand = exec.Command }() + + f := conflict.File{AbsolutePath: "testdata/lorem_ipsum"} + err := f.Read() + testhelper.Ok(t, err) + + c := conflict.Conflict{File: &f, Start: 0, End: 5} + output, err := Open(&c) + + testhelper.Ok(t, err) + testhelper.Equals(t, f.Lines, output) +} + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + os.Exit(0) +} + +// Allows us to mock exec.Command, thanks to +// https://npf.io/2015/06/testing-exec-command/ +func mockExecCommand(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestHelperProcess", "--", command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") + return cmd +}