Skip to content

Commit

Permalink
Merge pull request sequelize#439 from exercism/nextercism-solution-path
Browse files Browse the repository at this point in the history
[nextercism] Figure out where to download an exercise to
  • Loading branch information
Katrina Owen authored Aug 10, 2017
2 parents 87bb596 + b52d0d7 commit c669a3f
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 0 deletions.
1 change: 1 addition & 0 deletions fixtures/is-solution-path/broken/.solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{,}
Empty file.
3 changes: 3 additions & 0 deletions fixtures/is-solution-path/yepp/.solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "abc"
}
1 change: 1 addition & 0 deletions fixtures/solution-path/creatures/gazelle-2/.solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "bbb"}
1 change: 1 addition & 0 deletions fixtures/solution-path/creatures/gazelle-3/.solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "ccc"}
1 change: 1 addition & 0 deletions fixtures/solution-path/creatures/gazelle/.solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "aaa"}
60 changes: 60 additions & 0 deletions workspace/workspace.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package workspace

import (
"fmt"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -102,3 +103,62 @@ func (ws Workspace) Locate(exercise string) ([]string, error) {
}
return paths, nil
}

// SolutionPath returns the full path where the exercise will be stored.
// By default this the directory name matches that of the exercise, but if
// a different solution already exists, then a numeric suffix will be added
// to the name.
func (ws Workspace) SolutionPath(exercise, solutionID string) (string, error) {
paths, err := ws.Locate(exercise)
if !IsNotExist(err) && err != nil {
return "", err
}

return ws.ResolveSolutionPath(paths, exercise, solutionID, IsSolutionPath)
}

// IsSolutionPath checks whether the given path contains the solution with the given ID.
func IsSolutionPath(solutionID, path string) (bool, error) {
s, err := NewSolution(path)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return s.ID == solutionID, nil
}

// ResolveSolutionPath determines the path for the given exercise solution.
// It will locate an existing path, or indicate the name of a new path, if this is a new solution.
func (ws Workspace) ResolveSolutionPath(paths []string, exercise, solutionID string, existsFn func(string, string) (bool, error)) (string, error) {
// Do we already have a directory for this solution?
for _, path := range paths {
ok, err := existsFn(solutionID, path)
if err != nil {
return "", err
}
if ok {
return path, nil
}
}
// If we didn't find the solution in one of the paths that
// were passed in, we're going to construct some new ones
// using a numeric suffix. Create a lookup table so we can
// reject constructed paths if they match existing ones.
m := map[string]bool{}
for _, path := range paths {
m[path] = true
}
suffix := 1
root := filepath.Join(ws.Dir, exercise)
path := root
for {
exists := m[path]
if !exists {
return path, nil
}
suffix++
path = fmt.Sprintf("%s-%d", root, suffix)
}
}
126 changes: 126 additions & 0 deletions workspace/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,129 @@ func TestLocate(t *testing.T) {
assert.Equal(t, test.out, dirs, test.desc)
}
}

func TestSolutionPath(t *testing.T) {
root := filepath.Join("..", "fixtures", "solution-path", "creatures")
ws := New(root)

// An existing exercise.
path, err := ws.SolutionPath("gazelle", "ccc")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(root, "gazelle-3"), path)

path, err = ws.SolutionPath("gazelle", "abc")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(root, "gazelle-4"), path)

// A new exercise.
path, err = ws.SolutionPath("lizard", "abc")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(root, "lizard"), path)
}

func TestIsSolutionPath(t *testing.T) {
root := filepath.Join("..", "fixtures", "is-solution-path")

ok, err := IsSolutionPath("abc", filepath.Join(root, "yepp"))
assert.NoError(t, err)
assert.True(t, ok)

// The ID has to actually match.
ok, err = IsSolutionPath("xxx", filepath.Join(root, "yepp"))
assert.NoError(t, err)
assert.False(t, ok)

ok, err = IsSolutionPath("abc", filepath.Join(root, "nope"))
assert.NoError(t, err)
assert.False(t, ok)

_, err = IsSolutionPath("abc", filepath.Join(root, "broken"))
assert.Error(t, err)
}

func TestResolveSolutionPath(t *testing.T) {
ws := New("tmp")

existsFn := func(solutionID, path string) (bool, error) {
pathToSolutionID := map[string]string{
filepath.Join(ws.Dir, "pig"): "xxx",
filepath.Join(ws.Dir, "gecko"): "aaa",
filepath.Join(ws.Dir, "gecko-2"): "xxx",
filepath.Join(ws.Dir, "gecko-3"): "ccc",
filepath.Join(ws.Dir, "bat"): "aaa",
filepath.Join(ws.Dir, "dog"): "aaa",
filepath.Join(ws.Dir, "dog-2"): "bbb",
filepath.Join(ws.Dir, "dog-3"): "ccc",
filepath.Join(ws.Dir, "rabbit"): "aaa",
filepath.Join(ws.Dir, "rabbit-2"): "bbb",
filepath.Join(ws.Dir, "rabbit-4"): "ccc",
}
return pathToSolutionID[path] == solutionID, nil
}

tests := []struct {
desc string
paths []string
exercise string
expected string
}{
{
desc: "If we don't have that exercise yet, it gets the default name.",
exercise: "duck",
paths: []string{},
expected: filepath.Join(ws.Dir, "duck"),
},
{
desc: "If we already have a directory for the solution in question, return it.",
exercise: "pig",
paths: []string{
filepath.Join(ws.Dir, "pig"),
},
expected: filepath.Join(ws.Dir, "pig"),
},
{
desc: "If we already have multiple solutions, and this is one of them, find it.",
exercise: "gecko",
paths: []string{
filepath.Join(ws.Dir, "gecko"),
filepath.Join(ws.Dir, "gecko-2"),
filepath.Join(ws.Dir, "gecko-3"),
},
expected: filepath.Join(ws.Dir, "gecko-2"),
},
{
desc: "If we already have a solution, but this is a new one, add a suffix.",
exercise: "bat",
paths: []string{
filepath.Join(ws.Dir, "bat"),
},
expected: filepath.Join(ws.Dir, "bat-2"),
},
{
desc: "If we already have multiple solutions, but this is a new one, add a new suffix.",
exercise: "dog",
paths: []string{
filepath.Join(ws.Dir, "dog"),
filepath.Join(ws.Dir, "dog-2"),
filepath.Join(ws.Dir, "dog-3"),
},
expected: filepath.Join(ws.Dir, "dog-4"),
},
{
desc: "Use the first available suffix.",
exercise: "rabbit",
paths: []string{
filepath.Join(ws.Dir, "rabbit"),
filepath.Join(ws.Dir, "rabbit-2"),
filepath.Join(ws.Dir, "rabbit-4"),
},
expected: filepath.Join(ws.Dir, "rabbit-3"),
},
}

for _, test := range tests {
path, err := ws.ResolveSolutionPath(test.paths, test.exercise, "xxx", existsFn)
assert.NoError(t, err, test.desc)
assert.Equal(t, test.expected, path, test.desc)
}
}

0 comments on commit c669a3f

Please sign in to comment.