-
-
Notifications
You must be signed in to change notification settings - Fork 362
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
Metadata hidden subdir #723
Metadata hidden subdir #723
Conversation
6845814
to
193d964
Compare
workspace/exercise_test.go
Outdated
@@ -17,9 +17,9 @@ func TestHasMetadata(t *testing.T) { | |||
exerciseA := Exercise{Root: ws, Track: "bogus-track", Slug: "apple"} | |||
exerciseB := Exercise{Root: ws, Track: "bogus-track", Slug: "banana"} | |||
|
|||
err = os.MkdirAll(filepath.Join(exerciseA.Filepath()), os.FileMode(0755)) | |||
err = os.MkdirAll(filepath.Join(exerciseA.Filepath(), ignoreSubdir), os.FileMode(0755)) |
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.
Tests having to know to create the .exercism
ignore subdirectory seems smelly. I'm not sure how to avoid it though.
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.
How about using https://golang.org/pkg/path/filepath/#Dir on the metadata path? That way if it ever changes, the test won't have to know.
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.
Good call. I think I fixed this up in all the tests that were explicitly knowing about metadata filepaths.
workspace/workspace_test.go
Outdated
err := os.MkdirAll(path, os.FileMode(0755)) | ||
assert.NoError(t, err) | ||
|
||
if path != a2 { | ||
if path != filepath.Join(a2, ignoreSubdir) { |
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.
Tests having to know to create the
.exercism
ignore subdirectory seems smelly. I'm not sure how to avoid it though.
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.
I was thinking os.MkdirAll
on the filepath.Dir
of the full metadata file filepath -- I'd rather do that than have to have the code know about the various pieces of the metadata file path.
workspace/solution.go
Outdated
return nil | ||
} | ||
|
||
func migrateLegacySolutionFile(legacyMetadataPath string, metadataPath string) error { |
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.
Should this live here? Also, I saw mention of a future doctor
command. Do we want this migration method exported and workable with a doctor command?
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.
Oooh, that would be nice. Let's leave it for now, and if/when the doctor command gets off the ground we can export it.
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.
Actually, having looked at the code, I think I'd like to export it now, but not for future use in the doctor
command, but rather to explicitly call the migration method from the submit
command.
cmd/submit_test.go
Outdated
|
||
_, err = os.Stat(expectedPathAfterMigration) | ||
assert.NoError(t, err) | ||
} |
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.
This looks good. Can we also check that there's no file at the legacy
location?
workspace/solution.go
Outdated
fmt.Fprintf(os.Stderr, "\nMigrated solution metadata to %s\n", metadataPath) | ||
} 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", metadataPath) |
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.
I think if they both exist, we delete the old one.
workspace/workspace_test.go
Outdated
err := os.MkdirAll(path, os.FileMode(0755)) | ||
assert.NoError(t, err) | ||
|
||
if path != a2 { | ||
if path != filepath.Join(a2, ignoreSubdir) { |
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.
I was thinking os.MkdirAll
on the filepath.Dir
of the full metadata file filepath -- I'd rather do that than have to have the code know about the various pieces of the metadata file path.
workspace/exercise.go
Outdated
@@ -37,7 +37,12 @@ func (e Exercise) Filepath() string { | |||
|
|||
// MetadataFilepath is the absolute path to the exercise metadata. | |||
func (e Exercise) MetadataFilepath() string { | |||
return filepath.Join(e.Filepath(), solutionFilename) | |||
return filepath.Join(e.Filepath(), ignoreSubdirMetadataFilepath()) |
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.
How would you feel about making the relative path to the metadata filepath a variable defined at compile time rather than a function call?
var metadataFilepath = filepath.Join(".exercism", "solution.json")
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.
Sure. I'd made it a function call as a workaround to keep const
.
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.
Ah right. Personally, I'd rather have a var than a function call
workspace/workspace.go
Outdated
@@ -113,9 +113,22 @@ 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 := checkMetadataFile(path); err == 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.
I'm not quite comfortable with how this method not only checks the files, but also does the migration.
What do you think about inlining the checking logic into this function, so that it explicitly checks the metadata file path and returns if it's there, then checks the legacy metadata file path and returns if it's there.
Then, after we've found the solution directory, call a workspace.MigrateLegacyMetadataFile()
function (or some such name), which is a noop if the metadata file isn't legacy.
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.
Yeah, I wasn't happy with this either. What you've suggested seems like the way to go.
workspace/solution.go
Outdated
return nil | ||
} | ||
|
||
func migrateLegacySolutionFile(legacyMetadataPath string, metadataPath string) error { |
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.
Actually, having looked at the code, I think I'd like to export it now, but not for future use in the doctor
command, but rather to explicitly call the migration method from the submit
command.
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.
I like where this is going. It's a surprisingly small amount of code, now!
I have a few suggestions, below.
cmd/submit_test.go
Outdated
@@ -434,3 +483,19 @@ 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) { |
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.
This is only used in one place, right? If so I'd rather inline it into the test.
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.
Right. Good call.
543c166
to
017a055
Compare
cmd/submit_test.go
Outdated
_, err = os.Stat(expectedPathAfterMigration) | ||
assert.NoError(t, err) | ||
_, err = os.Stat(exercise.LegacyMetadataFilepath()) | ||
assert.Error(t, err) |
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.
Added check that legacy no longer exists.
017a055
to
9752d62
Compare
Inline creating fake legacy metadata and assert legacy location DNE after migration
Passing TODO: * if legacy & modern metadata exist, handle deletion * unit tests for migration
cc2f0b1
to
4c68d61
Compare
I believe all the review concerns are now implemented. I also attempted writing unit tests for the migration (I assume we want unit tests since it's exported and potentially dangerous). I think it's ready for another round. |
4c68d61
to
49178ce
Compare
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.
I definitely like the change to make the migration explicit. I have a few more comments here, but I think we're close.
workspace/exercise.go
Outdated
|
||
// MigrateLegacyMetadataFile migrates a legacy metadata to the modern location. | ||
// This is a noop if the metadata file isn't legacy. | ||
// If both legacy and modern metadata files exist, the legacy file will be deleted. |
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.
👍 😍 This is such a great comment.
workspace/exercise.go
Outdated
return "", err | ||
} | ||
str = fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath) | ||
fmt.Fprintf(os.Stderr, str) |
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.
I'd rather not print anything in packages other than the cmd
package. Let's make this return some sort of state or conclusion (a custom type, maybe an iota?) that the command package (or whoever) can check to decide whether or not to print stuff.
workspace/exercise.go
Outdated
// This is a noop if the metadata file isn't legacy. | ||
// If both legacy and modern metadata files exist, the legacy file will be deleted. | ||
func (e Exercise) MigrateLegacyMetadataFile() (string, error) { | ||
var str string |
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.
The str
variable doesn't seem to be doing anything at the moment. Can we ditch it?
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.
I added it for use in the unit tests. The thought was to increase test granularity: just asserting no error isn't enough to distinguish if the legacy was moved vs deleted. Based on your feedback above, it sounds like it should be turned into some kind of status type. I'm not sure if you saw you unit tests. I'll make another comment.
workspace/exercise.go
Outdated
str = fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath) | ||
fmt.Fprintf(os.Stderr, str) | ||
} | ||
return str, 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.
I think we can get rid of the else
and move everything from the else down here.
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.
Yeah, the else can be removed using early returns. I changed that here b555a3a.
workspace/exercise.go
Outdated
metadataFilepath := e.MetadataFilepath() | ||
|
||
if _, err := os.Lstat(legacyMetadataFilepath); err != nil { | ||
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.
Is the assumption here that if we get an error, it's an os.IsNotExist
error? Is that a safe assumption?
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.
Yeah, this is problematic. I replaced this by calling exercise.HasMetadata() since it contains the logic needed here. The trade-off is that this adds excess calls to MetadataFilepath() but it might be worth it for improved readability and avoiding duplication?
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.
The trade-off is that this adds excess calls to MetadataFilepath() but it might be worth it for improved readability and avoiding duplication?
Yeah, I wouldn't worry about that. If we find that we have performance problems we can do proper profiling etc to see what's up and fix it 👍
workspace/solution.go
Outdated
visibility.ShowFile(path) | ||
|
||
if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil { | ||
if err = createIgnoreSubdir(dir); err != 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.
I'd probably inline this. I know createIgnoreSubdir
is used in a couple of places, but it doesn't feel like an actual abstraction to me, just an accidental bit of logic that happens to have to happen twice.
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.
Agreed. As you mentioned below, using MkdirAll
makes this just 2 lines.
workspace/solution.go
Outdated
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 { |
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.
If you use MkdirAll
instead of Mkdir
you don't have to check that it doesn't exist first.
workspace/exercise_test.go
Outdated
@@ -42,3 +68,86 @@ func TestNewFromDir(t *testing.T) { | |||
assert.Equal(t, "the-track", exercise.Track) | |||
assert.Equal(t, "the-exercise", exercise.Slug) | |||
} | |||
|
|||
func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { |
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.
Are these tests ok? I wasn't sure if this is overboard.
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.
Yeah, I think these are great. I'm all for explicit, thorough tests :)
You could try turning it into a single table test if you wanted to--not sure what that would end up looking like (sometimes it's better, sometimes it's worth).
WIP - will replace with status
The logic for checking metadata files in the Migration method exists in Exercise's HasMetadata(). I previously avoided using these methods because this adds duplicate calls to MetadataFilepath() but this might be worth trading for DRYing up the file checking logic.
workspace/exercise.go
Outdated
MigrationStatusRemoved | ||
) | ||
|
||
func (m MigrationStatus) String(e Exercise) string { |
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.
Is it ok that this takes an Exercise
? I'm not sure if there's a better way to do this. Making it a method on Exercise
doesn't seem right.
Is it necessary to include the metadata location?
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.
Making it a method on Exercise doesn't seem right.
Why is that? I actually think that seems fine, but I fear that I'm missing something.
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.
No, let's not include the metadata location.
5d85fae
to
5d4e34b
Compare
Unify the assertions on metadata existing
'dir' makes more sense now using Exercise.NewExerciseFromDir
Replace knowledge of ignoreSubdir with encapsulated Exercise
workspace/solution.go
Outdated
visibility.ShowFile(path) | ||
|
||
if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil { | ||
metadataAbsoluteFilepath := NewExerciseFromDir(dir).MetadataFilepath() |
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.
I named this metadataAbsoluteFilepath
rather than metadataFilepath
because it would shadow the var in Solution.
if _, err := os.Lstat(filepath.Join(path, metadataFilepath)); err == nil { | ||
return path, nil | ||
} | ||
if _, err := os.Lstat(filepath.Join(path, legacySolutionFilename)); err == 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.
Is it ok that above we're using the name metadataFilepath
, contrasting with legacySolutionFilename
here? Will we presumably clean up all related solution/metadata incoherencies in the future rename PR?
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.
Will we presumably clean up all related solution/metadata incoherencies in the future rename PR?
Yeah, I think it makes sense to do that separately.
07617a4
to
839a2d4
Compare
Knowledge of metadata filepath doesn't require asking Exercise
Reduce needless complexity
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.
This looks great! There's just the one thing that you pointed out about the migration status String() method.
cmd/submit.go
Outdated
return err | ||
} | ||
if verbose, _ := flags.GetBool("verbose"); verbose { | ||
fmt.Fprintf(os.Stderr, migrationStatus.String(exercise)) |
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.
Oh, I see what you mean by it seems wrong that it takes an exercise! Yes, somehow I didn't see that it was the String()
method.
Yes, I agree that this isn't ideal.
workspace/exercise.go
Outdated
MigrationStatusRemoved | ||
) | ||
|
||
func (m MigrationStatus) String(e Exercise) string { |
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.
No, let's not include the metadata location.
I think it's ready. Do you want me to squash some of this? |
I'll go ahead and squash and merge on my end. Thank you ever so much for your patience in working through this! ❤️ 💛 💚 💙 💜 🖤 |
Cheers. It was a great learning experience. |
@jdsutherland great stuff on getting this in. I'm glad @kytrinyx was able to help you get this into a good place. With that said, we should probably test, maybe as a pre-release, to ensure exercises get migrated over successfully. @kytrinyx thoughts? |
Yepp--we can test this by building locally:
Then configuring with @jdsutherland want to give this a first run? |
@kytrinyx I was thinking of making a pre-release version available and asking members of the Exercism group to test as well; if they are up for it. Is that okay with you? |
I tested this locally and verified stderr output cases were correct without any issues. I don't have an exhaustive local workspace to test widely though. |
@nywilken that would be great. |
@jdsutherland @kytrinyx I pushed out an alpha release so that we can test this furhter. Please feel free to download and give it a run (https://github.com/exercism/cli/releases/tag/v3.0.10-alpha.1). There are no breaking changes here, but I would like to document how to roll back changes once done with the testing. Seeing as 3.0.9 binaries won't find the newly migrated metadata file. I will make an official issue in the AM with testing steps. |
A shell script could create a copy workspace to test with to avoid touching the default workspace. Are you suggesting a rollback mechanism if there is an issue after live rollout? What do you have in mind? Side note: IIRC, we deleted the |
Continuation of #630
Closes #630
Closes #607
Closes #557