diff --git a/cmd/download.go b/cmd/download.go index 0e782d40..031ba877 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -60,21 +60,19 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err != nil { return err } - exercise, err := flags.GetString("exercise") + slug, err := flags.GetString("exercise") if err != nil { return err } - if uuid == "" && exercise == "" { + if uuid == "" && slug == "" { return errors.New("need an --exercise name or a solution --uuid") } - var slug string - if uuid == "" { - slug = "latest" - } else { - slug = uuid + param := "latest" + if param == "" { + param = uuid } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), slug) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), param) client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { @@ -98,7 +96,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { if uuid == "" { q := req.URL.Query() - q.Add("exercise_id", exercise) + q.Add("exercise_id", slug) if track != "" { q.Add("track_id", track) } @@ -144,28 +142,26 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { IsRequester: payload.Solution.User.IsRequester, } - dir := usrCfg.GetString("workspace") + root := usrCfg.GetString("workspace") if solution.Team != "" { - dir = filepath.Join(dir, "teams", solution.Team) + root = filepath.Join(root, "teams", solution.Team) } if !solution.IsRequester { - dir = filepath.Join(dir, "users", solution.Handle) + root = filepath.Join(root, "users", solution.Handle) } - dir = filepath.Join(dir, solution.Track) - os.MkdirAll(dir, os.FileMode(0755)) - ws, err := workspace.New(dir) - if err != nil { - return err + exercise := workspace.Exercise{ + Root: root, + Track: solution.Track, + Slug: solution.Exercise, } - dir, err = ws.SolutionPath(solution.Exercise, solution.ID) - if err != nil { + dir := exercise.MetadataDir() + + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return err } - os.MkdirAll(dir, os.FileMode(0755)) - err = solution.Write(dir) if err != nil { return err diff --git a/cmd/open.go b/cmd/open.go index 0da33b07..15166b25 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -1,15 +1,9 @@ package cmd import ( - "errors" - "fmt" - "github.com/exercism/cli/browser" - "github.com/exercism/cli/comms" - "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // openCmd opens the designated exercise in the browser. @@ -19,74 +13,16 @@ var openCmd = &cobra.Command{ Short: "Open an exercise on the website.", Long: `Open the specified exercise to the solution page on the Exercism website. -Pass either the name of an exercise, or the path to the directory that contains -the solution you want to see on the website. +Pass the path to the directory that contains the solution you want to see on the website. `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfig() - - v := viper.New() - v.AddConfigPath(cfg.Dir) - v.SetConfigName("user") - v.SetConfigType("json") - // Ignore error. If the file doesn't exist, that is fine. - _ = v.ReadInConfig() - - ws, err := workspace.New(v.GetString("workspace")) - if err != nil { - return err - } - - paths, err := ws.Locate(args[0]) + solution, err := workspace.NewSolution(args[0]) if err != nil { return err } - - solutions, err := workspace.NewSolutions(paths) - if err != nil { - return err - } - - if len(solutions) == 0 { - return nil - } - - if len(solutions) > 1 { - var mine []*workspace.Solution - for _, s := range solutions { - if s.IsRequester { - mine = append(mine, s) - } - } - solutions = mine - } - - selection := comms.NewSelection() - for _, solution := range solutions { - selection.Items = append(selection.Items, solution) - } - for { - prompt := ` -We found more than one. Which one did you mean? -Type the number of the one you want to select. - -%s -> ` - option, err := selection.Pick(prompt) - if err != nil { - fmt.Println(err) - continue - } - solution, ok := option.(*workspace.Solution) - if ok { - browser.Open(solution.URL) - return nil - } - if err != nil { - return errors.New("should never happen") - } - } + browser.Open(solution.URL) + return nil }, } diff --git a/cmd/submit.go b/cmd/submit.go index 2028c940..50780840 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,26 +125,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { exerciseDir = dir } - dirs, err := ws.Locate(exerciseDir) + solution, err := workspace.NewSolution(exerciseDir) if err != nil { return err } - sx, err := workspace.NewSolutions(dirs) - if err != nil { - return err - } - if len(sx) > 1 { - msg := ` - - You are submitting files belonging to different solutions. - Please submit the files for one solution at a time. - - ` - return errors.New(msg) - } - solution := sx[0] - if !solution.IsRequester { // TODO: add test msg := ` diff --git a/comms/question.go b/comms/question.go deleted file mode 100644 index f9afd6a1..00000000 --- a/comms/question.go +++ /dev/null @@ -1,36 +0,0 @@ -package comms - -import ( - "bufio" - "fmt" - "io" - "strings" -) - -// Question provides an interactive session. -type Question struct { - Reader io.Reader - Writer io.Writer - Prompt string - DefaultValue string -} - -// Read reads the user's input. -func (q Question) Read(r io.Reader) (string, error) { - reader := bufio.NewReader(r) - s, err := reader.ReadString('\n') - if err != nil { - return "", err - } - s = strings.TrimSpace(s) - if s == "" { - return q.DefaultValue, nil - } - return s, nil -} - -// Ask displays the prompt, then records the response. -func (q *Question) Ask() (string, error) { - fmt.Fprintf(q.Writer, q.Prompt) - return q.Read(q.Reader) -} diff --git a/comms/question_test.go b/comms/question_test.go deleted file mode 100644 index 1b77dad6..00000000 --- a/comms/question_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package comms - -import ( - "io/ioutil" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestQuestion(t *testing.T) { - testCases := []struct { - desc string - given string - fallback string - expected string - }{ - {"records interactive response", "hello\n", "", "hello"}, - {"responds with default if response is empty", "\n", "Fine.", "Fine."}, - {"removes trailing \\r in addition to trailing \\", "hello\r\n", "Fine.", "hello"}, - {"removes trailing white spaces", "hello \n", "Fine.", "hello"}, - {"falls back to default value", " \n", "Default", "Default"}, - } - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - q := &Question{ - Reader: strings.NewReader(tc.given), - Writer: ioutil.Discard, - Prompt: "Say something: ", - DefaultValue: tc.fallback, - } - - answer, err := q.Ask() - assert.NoError(t, err) - assert.Equal(t, answer, tc.expected) - }) - } -} diff --git a/comms/selection.go b/comms/selection.go deleted file mode 100644 index 5d760864..00000000 --- a/comms/selection.go +++ /dev/null @@ -1,78 +0,0 @@ -package comms - -import ( - "bufio" - "errors" - "fmt" - "io" - "os" - "strconv" - "strings" -) - -// Selection wraps a list of items. -// It is used for interactive communication. -type Selection struct { - Items []fmt.Stringer - Reader io.Reader - Writer io.Writer -} - -// NewSelection prepares an empty collection for interactive input. -func NewSelection() Selection { - return Selection{ - Reader: os.Stdin, - Writer: os.Stdout, - } -} - -// Pick lets a user interactively select an option from a list. -func (sel Selection) Pick(prompt string) (fmt.Stringer, error) { - // If there's just one, then we're done here. - if len(sel.Items) == 1 { - return sel.Items[0], nil - } - - fmt.Fprintf(sel.Writer, prompt, sel.Display()) - - n, err := sel.Read(sel.Reader) - if err != nil { - return nil, err - } - - o, err := sel.Get(n) - if err != nil { - return nil, err - } - return o, nil -} - -// Display shows a numbered list of the solutions to choose from. -// The list starts at 1, since that seems better in a user interface. -func (sel Selection) Display() string { - str := "" - for i, item := range sel.Items { - str += fmt.Sprintf(" [%d] %s\n", i+1, item) - } - return str -} - -// Read reads the user's selection and converts it to a number. -func (sel Selection) Read(r io.Reader) (int, error) { - reader := bufio.NewReader(r) - text, _ := reader.ReadString('\n') - n, err := strconv.Atoi(strings.TrimSpace(text)) - if err != nil { - return 0, err - } - return n, nil -} - -// Get returns the solution corresponding to the number. -// The list starts at 1, since that seems better in a user interface. -func (sel Selection) Get(n int) (fmt.Stringer, error) { - if n <= 0 || n > len(sel.Items) { - return nil, errors.New("we don't have that one") - } - return sel.Items[n-1], nil -} diff --git a/comms/selection_test.go b/comms/selection_test.go deleted file mode 100644 index bbf2ce16..00000000 --- a/comms/selection_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package comms - -import ( - "fmt" - "io/ioutil" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -type thing struct { - name string - rating int -} - -func (t thing) String() string { - return fmt.Sprintf("%s (+%d)", t.name, t.rating) -} - -var ( - things = []thing{ - {name: "water", rating: 10}, - {name: "food", rating: 3}, - {name: "music", rating: 0}, - } -) - -func TestSelectionDisplay(t *testing.T) { - // We have to manually add each thing to the options collection. - var sel Selection - for _, thing := range things { - sel.Items = append(sel.Items, thing) - } - - display := " [1] water (+10)\n [2] food (+3)\n [3] music (+0)\n" - assert.Equal(t, display, sel.Display()) -} - -func TestSelectionGet(t *testing.T) { - var sel Selection - for _, thing := range things { - sel.Items = append(sel.Items, thing) - } - - _, err := sel.Get(0) - assert.Error(t, err) - - o, err := sel.Get(1) - assert.NoError(t, err) - // We need to do a type assertion to access - // any non-stringer stuff. - t1 := o.(thing) - assert.Equal(t, "water", t1.name) - - o, err = sel.Get(2) - assert.NoError(t, err) - t2 := o.(thing) - assert.Equal(t, "food", t2.name) - - o, err = sel.Get(3) - assert.NoError(t, err) - t3 := o.(thing) - assert.Equal(t, "music", t3.name) - - _, err = sel.Get(4) - assert.Error(t, err) -} - -func TestSelectionRead(t *testing.T) { - var sel Selection - n, err := sel.Read(strings.NewReader("5")) - assert.NoError(t, err) - assert.Equal(t, 5, n) - - _, err = sel.Read(strings.NewReader("abc")) - assert.Error(t, err) -} - -func TestSelectionPick(t *testing.T) { - testCases := []struct { - desc string - selection Selection - things []thing - expected string - }{ - { - desc: "autoselect the only one", - selection: Selection{ - // it never hits the error, - // because it doesn't actually do - // the prompt and read response. - Reader: strings.NewReader("BOOM!"), - }, - things: []thing{ - {"hugs", 100}, - }, - expected: "hugs", - }, - { - desc: "it picks the one corresponding to the selection", - selection: Selection{ - Reader: strings.NewReader("2"), - }, - things: []thing{ - {"food", 10}, - {"water", 3}, - {"music", 0}, - }, - expected: "water", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - tc.selection.Writer = ioutil.Discard - for _, th := range tc.things { - tc.selection.Items = append(tc.selection.Items, th) - } - - item, err := tc.selection.Pick("which one? %s") - assert.NoError(t, err) - th, ok := item.(thing) - assert.True(t, ok) - assert.Equal(t, tc.expected, th.name) - }) - } -} diff --git a/workspace/exercise.go b/workspace/exercise.go index e8be3eca..246cf364 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -30,6 +30,12 @@ func (e Exercise) MetadataFilepath() string { return filepath.Join(e.Filepath(), solutionFilename) } +// MetadataDir returns the directory that the exercise metadata lives in. +// For now this is the exercise directory. +func (e Exercise) MetadataDir() string { + return e.Filepath() +} + // HasMetadata checks for the presence of an exercise metadata file. // If there is no such file, this may be a legacy exercise. // It could also be an unrelated directory. diff --git a/workspace/workspace.go b/workspace/workspace.go index b6a37854..0e9092e4 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -2,7 +2,6 @@ package workspace import ( "errors" - "fmt" "io/ioutil" "os" "path/filepath" @@ -99,151 +98,6 @@ func (ws Workspace) Exercises() ([]Exercise, error) { return exercises, nil } -// Locate the matching directories within the workspace. -// This will look for an exact match on absolute or relative paths. -// If given the base name of a directory with no path information it -// It will look for all directories with that name, or that are -// named with a numerical suffix. -func (ws Workspace) Locate(exercise string) ([]string, error) { - // First assume it's a path. - dir := exercise - - // If it's not an absolute path, make it one. - if !filepath.IsAbs(dir) { - var err error - dir, err = filepath.Abs(dir) - if err != nil { - return nil, err - } - } - - // If it exists, we were right. It's a path. - if _, err := os.Stat(dir); err == nil { - if !strings.HasPrefix(dir, ws.Dir) { - return nil, ErrNotInWorkspace(exercise) - } - - src, err := filepath.EvalSymlinks(dir) - if err == nil { - return []string{src}, nil - } - } - - // If the argument is a path, then we should have found it by now. - if strings.Contains(exercise, string(os.PathSeparator)) { - return nil, ErrNotExist(exercise) - } - - var paths []string - // Look through the entire workspace tree to find any matches. - walkFn := func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // If it's a symlink, follow it, then get the file info of the target. - if info.Mode()&os.ModeSymlink == os.ModeSymlink { - src, err := filepath.EvalSymlinks(path) - if err == nil { - path = src - } - info, err = os.Lstat(path) - if err != nil { - return err - } - } - - if !info.IsDir() { - return nil - } - - if strings.HasPrefix(filepath.Base(path), exercise) { - // We're trying to find any directories that match either the exact name - // or the name with a numeric suffix. - // E.g. if passed 'bat', then we should match 'bat', 'bat-2', 'bat-200', - // but not 'batten'. - suffix := strings.Replace(filepath.Base(path), exercise, "", 1) - if suffix == "" || rgxSerialSuffix.MatchString(suffix) { - paths = append(paths, path) - } - } - return nil - } - - // If the workspace directory is a symlink, resolve that first. - root := ws.Dir - src, err := filepath.EvalSymlinks(root) - if err == nil { - root = src - } - - filepath.Walk(root, walkFn) - - if len(paths) == 0 { - return nil, ErrNotExist(exercise) - } - 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) - } -} - // SolutionDir determines the root directory of a solution. // This is the directory that contains the solution metadata file. func (ws Workspace) SolutionDir(s string) (string, error) { diff --git a/workspace/workspace_locate_test.go b/workspace/workspace_locate_test.go deleted file mode 100644 index 837657b9..00000000 --- a/workspace/workspace_locate_test.go +++ /dev/null @@ -1,139 +0,0 @@ -// +build !windows - -package workspace - -import ( - "fmt" - "path/filepath" - "runtime" - "sort" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLocateErrors(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - ws, err := New(filepath.Join(root, "workspace")) - assert.NoError(t, err) - - testCases := []struct { - desc, arg string - errFn func(error) bool - }{ - { - desc: "absolute path outside of workspace", - arg: filepath.Join(root, "equipment", "bat"), - errFn: IsNotInWorkspace, - }, - { - desc: "absolute path in workspace not found", - arg: filepath.Join(ws.Dir, "creatures", "pig"), - errFn: IsNotExist, - }, - { - desc: "relative path is outside of workspace", - arg: filepath.Join("..", "fixtures", "locate-exercise", "equipment", "bat"), - errFn: IsNotInWorkspace, - }, - { - desc: "relative path in workspace not found", - arg: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "pig"), - errFn: IsNotExist, - }, - { - desc: "exercise name not found in workspace", - arg: "pig", - errFn: IsNotExist, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - _, err := ws.Locate(tc.arg) - assert.True(t, tc.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", tc.desc, tc.arg, err)) - }) - } -} - -type locateTestCase struct { - desc string - workspace Workspace - in string - out []string -} - -func TestLocate(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - wsPrimary, err := New(filepath.Join(root, "workspace")) - assert.NoError(t, err) - - testCases := []locateTestCase{ - { - desc: "find absolute path within workspace", - workspace: wsPrimary, - in: filepath.Join(wsPrimary.Dir, "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find relative path within workspace", - workspace: wsPrimary, - in: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in default location", - workspace: wsPrimary, - in: "horse", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in a subtree", - workspace: wsPrimary, - in: "fly", - out: []string{filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "fly")}, - }, - { - desc: "don't be confused by a file named the same as an exercise", - workspace: wsPrimary, - in: "duck", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "duck")}, - }, - { - desc: "find all the exercises with the same name", - workspace: wsPrimary, - in: "bat", - out: []string{ - filepath.Join(wsPrimary.Dir, "creatures", "bat"), - filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "bat"), - }, - }, - { - desc: "find copies of exercise with suffix", - workspace: wsPrimary, - in: "crane", - out: []string{ - filepath.Join(wsPrimary.Dir, "creatures", "crane"), - filepath.Join(wsPrimary.Dir, "creatures", "crane-2"), - }, - }, - } - - testLocate(testCases, t) -} - -func testLocate(testCases []locateTestCase, t *testing.T) { - for _, tc := range testCases { - dirs, err := tc.workspace.Locate(tc.in) - - sort.Strings(dirs) - sort.Strings(tc.out) - - assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.out, dirs, tc.desc) - } -} diff --git a/workspace/workspace_symlinks_test.go b/workspace/workspace_symlinks_test.go deleted file mode 100644 index 83718369..00000000 --- a/workspace/workspace_symlinks_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// +build !windows - -package workspace - -import ( - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLocateSymlinks(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - wsSymbolic, err := New(filepath.Join(root, "symlinked-workspace")) - assert.NoError(t, err) - wsPrimary, err := New(filepath.Join(root, "workspace")) - assert.NoError(t, err) - - testCases := []locateTestCase{ - { - desc: "find absolute path within symlinked workspace", - workspace: wsSymbolic, - in: filepath.Join(wsSymbolic.Dir, "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in a symlinked workspace", - workspace: wsSymbolic, - in: "horse", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "don't be confused by a symlinked file named the same as an exercise", - workspace: wsPrimary, - in: "date", - out: []string{filepath.Join(wsPrimary.Dir, "actions", "date")}, - }, - { - desc: "find exercises that are symlinks", - workspace: wsPrimary, - in: "squash", - out: []string{ - filepath.Join(wsPrimary.Dir, "..", "food", "squash"), - filepath.Join(wsPrimary.Dir, "actions", "squash"), - }, - }, - } - - testLocate(testCases, t) -} diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 31097b61..b48f3cf3 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -87,136 +87,6 @@ func TestWorkspaceExercises(t *testing.T) { } } -func TestSolutionPath(t *testing.T) { - root := filepath.Join("..", "fixtures", "solution-path", "creatures") - ws, err := New(root) - assert.NoError(t, err) - - // 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) { - tmpDir, err := ioutil.TempDir("", "resolve-solution-path") - defer os.RemoveAll(tmpDir) - ws, err := New(tmpDir) - assert.NoError(t, err) - - 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) - } -} - func TestSolutionDir(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "solution-dir")