From 1bb612f326ba3b87588c7c19ff0128eb539c922d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 7 Aug 2018 16:19:10 -0600 Subject: [PATCH] Get rid of the workspace.Locate behavior and comms package (#696) * Simplify open command This no longer accepts just an exercise name. It mirrors the behavior of the submit command where you pass the path to the exercise you want to open in the browser. In the future we can expand this to take --exercise, --track, and --team. * Simplify submit command We determine the exact exercise directory based on the filepaths. Since we have the directory, we don't need to do complicated guessing, we can just load in the exercise metadata directly. * Rename slug to param in download command for clarity * Rename exercise to slug in download for clarity * Rename dir variable in download command for clarity We were overloading the variable, which makes it confusing. * Add missing error handling in download command * Add metadata dir method to exercise type We're going to want to ensure that the directory exists. For the moment, this directory is the same as the exercise directory, but we're working towards putting this in a subdirectory to match common industry conventions. * Simplify download command to rely on Exercise type * Get rid of solution path abstraction in workspace We've moved the idea of the exercise metadata path onto the Exercise type. * Delete complicated Locate behavior in workspace It's no longer used. * Delete unused comms package * Tweak download command for clarity Simplify a conditional. --- cmd/download.go | 38 ++++--- cmd/open.go | 72 +------------ cmd/submit.go | 17 +--- comms/question.go | 36 ------- comms/question_test.go | 38 ------- comms/selection.go | 78 -------------- comms/selection_test.go | 128 ----------------------- workspace/exercise.go | 6 ++ workspace/workspace.go | 146 --------------------------- workspace/workspace_locate_test.go | 139 ------------------------- workspace/workspace_symlinks_test.go | 53 ---------- workspace/workspace_test.go | 130 ------------------------ 12 files changed, 28 insertions(+), 853 deletions(-) delete mode 100644 comms/question.go delete mode 100644 comms/question_test.go delete mode 100644 comms/selection.go delete mode 100644 comms/selection_test.go delete mode 100644 workspace/workspace_locate_test.go delete mode 100644 workspace/workspace_symlinks_test.go 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")