Skip to content
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

Merged
merged 32 commits into from
Sep 4, 2018
Merged

Conversation

jdsutherland
Copy link
Contributor

@jdsutherland jdsutherland commented Aug 27, 2018

Continuation of #630
Closes #630
Closes #607
Closes #557

@@ -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))
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

err := os.MkdirAll(path, os.FileMode(0755))
assert.NoError(t, err)

if path != a2 {
if path != filepath.Join(a2, ignoreSubdir) {
Copy link
Contributor Author

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.

Copy link
Member

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.

return nil
}

func migrateLegacySolutionFile(legacyMetadataPath string, metadataPath string) error {
Copy link
Contributor Author

@jdsutherland jdsutherland Aug 27, 2018

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?

Copy link
Member

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.

Copy link
Member

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.


_, err = os.Stat(expectedPathAfterMigration)
assert.NoError(t, err)
}
Copy link
Member

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?

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)
Copy link
Member

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.

err := os.MkdirAll(path, os.FileMode(0755))
assert.NoError(t, err)

if path != a2 {
if path != filepath.Join(a2, ignoreSubdir) {
Copy link
Member

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.

@@ -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())
Copy link
Member

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")

Copy link
Contributor Author

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.

Copy link
Member

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

@@ -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 {
Copy link
Member

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.

Copy link
Contributor Author

@jdsutherland jdsutherland Aug 28, 2018

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.

return nil
}

func migrateLegacySolutionFile(legacyMetadataPath string, metadataPath string) error {
Copy link
Member

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.

Copy link
Member

@kytrinyx kytrinyx left a 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.

@@ -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) {
Copy link
Member

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.

Copy link
Contributor Author

@jdsutherland jdsutherland Aug 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Good call.

@jdsutherland jdsutherland force-pushed the metadata-hidden-subdir branch 2 times, most recently from 543c166 to 017a055 Compare August 28, 2018 04:36
_, err = os.Stat(expectedPathAfterMigration)
assert.NoError(t, err)
_, err = os.Stat(exercise.LegacyMetadataFilepath())
assert.Error(t, err)
Copy link
Contributor Author

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.

@jdsutherland jdsutherland force-pushed the metadata-hidden-subdir branch from 017a055 to 9752d62 Compare August 28, 2018 07:14
@jdsutherland jdsutherland force-pushed the metadata-hidden-subdir branch from cc2f0b1 to 4c68d61 Compare August 28, 2018 12:35
@jdsutherland
Copy link
Contributor Author

jdsutherland commented Aug 28, 2018

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.

@jdsutherland jdsutherland force-pushed the metadata-hidden-subdir branch from 4c68d61 to 49178ce Compare August 28, 2018 13:32
Copy link
Member

@kytrinyx kytrinyx left a 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.


// 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.
Copy link
Member

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.

return "", err
}
str = fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath)
fmt.Fprintf(os.Stderr, str)
Copy link
Member

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.

// 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
Copy link
Member

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?

Copy link
Contributor Author

@jdsutherland jdsutherland Aug 29, 2018

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.

str = fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath)
fmt.Fprintf(os.Stderr, str)
}
return str, nil
Copy link
Member

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.

Copy link
Contributor Author

@jdsutherland jdsutherland Aug 29, 2018

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.

metadataFilepath := e.MetadataFilepath()

if _, err := os.Lstat(legacyMetadataFilepath); err != nil {
return "", nil
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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 👍

visibility.ShowFile(path)

if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil {
if err = createIgnoreSubdir(dir); err != nil {
Copy link
Member

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.

Copy link
Contributor Author

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.

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 {
Copy link
Member

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.

@@ -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) {
Copy link
Contributor Author

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.

Copy link
Member

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.
MigrationStatusRemoved
)

func (m MigrationStatus) String(e Exercise) string {
Copy link
Contributor Author

@jdsutherland jdsutherland Aug 30, 2018

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?

Copy link
Member

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.

Copy link
Member

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.

@jdsutherland jdsutherland force-pushed the metadata-hidden-subdir branch from 5d85fae to 5d4e34b Compare August 30, 2018 04:06
Unify the assertions on metadata existing
'dir' makes more sense now using Exercise.NewExerciseFromDir
Replace knowledge of ignoreSubdir with encapsulated Exercise
visibility.ShowFile(path)

if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil {
metadataAbsoluteFilepath := NewExerciseFromDir(dir).MetadataFilepath()
Copy link
Contributor Author

@jdsutherland jdsutherland Aug 30, 2018

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 {
Copy link
Contributor Author

@jdsutherland jdsutherland Aug 30, 2018

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?

Copy link
Member

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.

@jdsutherland jdsutherland force-pushed the metadata-hidden-subdir branch from 07617a4 to 839a2d4 Compare August 31, 2018 03:11
Knowledge of metadata filepath doesn't require asking Exercise
Reduce needless complexity
Copy link
Member

@kytrinyx kytrinyx left a 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))
Copy link
Member

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.

MigrationStatusRemoved
)

func (m MigrationStatus) String(e Exercise) string {
Copy link
Member

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.

@jdsutherland
Copy link
Contributor Author

I think it's ready. Do you want me to squash some of this?

@kytrinyx
Copy link
Member

kytrinyx commented Sep 4, 2018

I'll go ahead and squash and merge on my end.

Thank you ever so much for your patience in working through this! ❤️ 💛 💚 💙 💜 🖤

@kytrinyx kytrinyx merged commit 5185642 into exercism:master Sep 4, 2018
@jdsutherland jdsutherland deleted the metadata-hidden-subdir branch September 5, 2018 02:23
@jdsutherland
Copy link
Contributor Author

Cheers. It was a great learning experience.

@nywilken
Copy link
Contributor

nywilken commented Sep 5, 2018

@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?

@kytrinyx
Copy link
Member

kytrinyx commented Sep 9, 2018

Yepp--we can test this by building locally:

go build -o testercism exercism/main.go

Then configuring with ./testercism configure, and then running a download and submit against the live site.

@jdsutherland want to give this a first run?

@nywilken
Copy link
Contributor

@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?

@jdsutherland
Copy link
Contributor Author

jdsutherland commented Sep 10, 2018

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.

@kytrinyx
Copy link
Member

@nywilken that would be great.

@nywilken
Copy link
Contributor

nywilken commented Sep 22, 2018

@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.

@jdsutherland
Copy link
Contributor Author

jdsutherland commented Sep 22, 2018

There are no breaking changes here, but I would like to document how to roll back changes once done with the testing.

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 visibility package related to hidden folders in Windows. Did/should we explicitly note to verify this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants