-
-
Notifications
You must be signed in to change notification settings - Fork 368
Move .solution.json to hidden subdirectory #630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
8f5493f
cef955f
60b57f1
34c4ec6
c76ab75
964e5c4
bea3009
4ad8121
642f601
858abd2
c363a1f
eaca5f3
5a641a5
9a2c772
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "io/ioutil" | ||
| "net/http" | ||
| "net/http/httptest" | ||
|
|
@@ -260,6 +261,50 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { | |
| assert.Regexp(t, "No files found", err.Error()) | ||
| } | ||
|
|
||
| func TestSubmitExerciseWithLegacySolutionMetadataFileAndGetsMigrated(t *testing.T) { | ||
| oldOut := Out | ||
| oldErr := Err | ||
| Out = ioutil.Discard | ||
| Err = ioutil.Discard | ||
| defer func() { | ||
| Out = oldOut | ||
| Err = oldErr | ||
| }() | ||
| // The fake endpoint will populate this when it receives the call from the command. | ||
| submittedFiles := map[string]string{} | ||
| ts := fakeSubmitServer(t, submittedFiles) | ||
| defer ts.Close() | ||
|
|
||
| tmpDir, err := ioutil.TempDir("", "legacy-metadata-file") | ||
| assert.NoError(t, err) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These files are not being cleaned up after the tests. A deferred call os.RemoveAll should do the trick. |
||
| dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") | ||
| os.MkdirAll(dir, os.FileMode(0755)) | ||
| writeFakeLegacySolution(t, dir, "bogus-track", "bogus-exercise") | ||
|
|
||
| file := filepath.Join(dir, "file.txt") | ||
| err = ioutil.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) | ||
| assert.NoError(t, err) | ||
|
|
||
| v := viper.New() | ||
| v.Set("token", "abc123") | ||
| v.Set("workspace", tmpDir) | ||
| v.Set("apibaseurl", ts.URL) | ||
| cfg := config.Configuration{ | ||
| Persister: config.InMemoryPersister{}, | ||
| Dir: tmpDir, | ||
| UserViperConfig: v, | ||
| } | ||
| expectedSolutionPathAfterMigration := filepath.Join(dir, workspace.SolutionMetadataFilepath()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to check that this file doesn’t exist prior to executing submit? @kytrinyx thoughts, Is it too much?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think it would help clarify intent and ensure completeness. |
||
|
|
||
| err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, "This is a file.", submittedFiles[string(os.PathSeparator)+"file.txt"]) | ||
|
|
||
| _, err = os.Stat(expectedSolutionPathAfterMigration) | ||
| assert.NoError(t, err) | ||
| } | ||
|
|
||
| func TestSubmitFilesFromDifferentSolutions(t *testing.T) { | ||
| tmpDir, err := ioutil.TempDir("", "dir-1-submit") | ||
| assert.NoError(t, err) | ||
|
|
@@ -331,3 +376,20 @@ func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { | |
| err := solution.Write(dir) | ||
| assert.NoError(t, err) | ||
| } | ||
|
|
||
| func writeFakeLegacySolution(t *testing.T, dir, trackID, exerciseSlug string) { | ||
| solution := &workspace.Solution{ | ||
| ID: "bogus-solution-uuid", | ||
| Track: trackID, | ||
| Exercise: exerciseSlug, | ||
| URL: "http://example.com/bogus-url", | ||
| IsRequester: true, | ||
| } | ||
| legacySolutionFilePath := filepath.Join(dir, ".solution.json") | ||
|
|
||
| b, err := json.Marshal(solution) | ||
| assert.NoError(t, err) | ||
|
|
||
| err = ioutil.WriteFile(legacySolutionFilePath, b, os.FileMode(0600)) | ||
| assert.NoError(t, err) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,11 +8,10 @@ import ( | |
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/exercism/cli/visibility" | ||
| ) | ||
|
|
||
| const solutionFilename = ".solution.json" | ||
| const ignoreSubdir = ".exercism" | ||
| const solutionFilename = "solution.json" | ||
|
|
||
| // Solution contains metadata about a user's solution. | ||
| type Solution struct { | ||
|
|
@@ -29,7 +28,7 @@ type Solution struct { | |
|
|
||
| // NewSolution reads solution metadata from a file in the given directory. | ||
| func NewSolution(dir string) (*Solution, error) { | ||
| path := filepath.Join(dir, solutionFilename) | ||
| path := filepath.Join(dir, SolutionMetadataFilepath()) | ||
| b, err := ioutil.ReadFile(path) | ||
| if err != nil { | ||
| return &Solution{}, err | ||
|
|
@@ -61,22 +60,19 @@ func (s *Solution) String() string { | |
| } | ||
|
|
||
| // Write stores solution metadata to a file. | ||
| func (s *Solution) Write(dir string) error { | ||
| func (s *Solution) Write(path string) error { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the confusion this variable should remain as I think what you have now is good. I just recommend that you keep |
||
| b, err := json.Marshal(s) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| path := filepath.Join(dir, solutionFilename) | ||
|
|
||
| // Hack because ioutil.WriteFile fails on hidden files | ||
| visibility.ShowFile(path) | ||
|
|
||
| if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil { | ||
| if err = createIgnoreSubdir(path); err != nil { | ||
| return err | ||
| } | ||
| s.Dir = dir | ||
| return visibility.HideFile(path) | ||
| if err = ioutil.WriteFile(filepath.Join(path, SolutionMetadataFilepath()), b, os.FileMode(0600)); err != nil { | ||
| return err | ||
| } | ||
| s.Dir = path | ||
| return err | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| // PathToParent is the relative path from the workspace to the parent dir. | ||
|
|
@@ -87,3 +83,37 @@ func (s *Solution) PathToParent() string { | |
| } | ||
| return filepath.Join(dir, s.Track) | ||
| } | ||
|
|
||
| // SolutionMetadataFilepath is the path of the solution metadata file relative to the workspace. | ||
| func SolutionMetadataFilepath() string { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand how this works. Each exercise has its own metadata file, so this is relative to a subdirectory within the workspace.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mistakenly used 'workspace' when I meant 'exercise' there.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, that makes sense. Thanks! |
||
| return filepath.Join(ignoreSubdir, solutionFilename) | ||
| } | ||
|
|
||
| func createIgnoreSubdir(path string) error { | ||
| path = filepath.Join(path, ignoreSubdir) | ||
| if _, err := os.Stat(path); os.IsNotExist(err) { | ||
| if err := os.Mkdir(path, os.FileMode(0755)); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func migrateLegacySolutionFile(legacySolutionPath string, solutionPath string) error { | ||
| if _, err := os.Lstat(legacySolutionPath); err != nil { | ||
| return err | ||
| } | ||
| if err := createIgnoreSubdir(filepath.Dir(legacySolutionPath)); err != nil { | ||
| return err | ||
| } | ||
| if _, err := os.Lstat(solutionPath); err != nil { | ||
| if err := os.Rename(legacySolutionPath, solutionPath); err != nil { | ||
| return err | ||
| } | ||
| fmt.Fprintf(os.Stderr, "\nMigrated solution metadata to %s\n", solutionPath) | ||
| } else { | ||
| // TODO: decide how to handle case where both legacy and modern metadata files exist | ||
| fmt.Fprintf(os.Stderr, "\nAttempted to migrate solution metadata to %s but file already exists\n", solutionPath) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the new metadata takes precendence which leads into my next comment about workspace,checkSolutionFile. It currently checks for a legacy metdata first, but I think we should check for the new path first and only check for legacy if it doesn’t exist.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. The most common path should be checked first. |
||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -198,9 +198,21 @@ func (ws Workspace) SolutionDir(s string) (string, error) { | |
| if _, err := os.Lstat(path); os.IsNotExist(err) { | ||
| return "", err | ||
| } | ||
| if _, err := os.Lstat(filepath.Join(path, solutionFilename)); err == nil { | ||
| if err := checkSolutionFile(path); err == nil { | ||
| return path, nil | ||
| } | ||
| path = filepath.Dir(path) | ||
| } | ||
| } | ||
|
|
||
| func checkSolutionFile(path string) error { | ||
| legacySolutionPath := filepath.Join(path, ".solution.json") | ||
| solutionPath := filepath.Join(path, SolutionMetadataFilepath()) | ||
|
|
||
| if _, err := os.Lstat(legacySolutionPath); err == nil { | ||
| return migrateLegacySolutionFile(legacySolutionPath, solutionPath) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } else if _, err := os.Lstat(solutionPath); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about calling this method
TestLegacySolutionMetadataMigration?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TestLegacySolutionMetadataMigrationis more readable. The only potential concern I can think of is that the test isn't just testing migration but that a submission is still successful given a legacy solution file - is it worth indicating that in the name?