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

Ensure HABTM relationships produce valid class names (Fixes #17119) #17217

Merged
merged 1 commit into from
Nov 9, 2014

Conversation

codeodor
Copy link
Contributor

@codeodor codeodor commented Oct 8, 2014

See #17119 for the back story.

@codeodor
Copy link
Contributor Author

codeodor commented Oct 9, 2014

I guess I need to figure out a fix that runs on Ruby 1.9.3 and 2.0.

@codeodor
Copy link
Contributor Author

codeodor commented Oct 9, 2014

Adjustment made to pass on 2.1, 2.0, and 1.9.3 in my environment. Now let's see how Travis does. 😄

# we've been able to reproduce one of the 2 parts of this bug
refute_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers")
Developer._reflections.values.each do |association|
constant = ObjectSpace.each_object(Class).detect{|c| c.to_s == association.class_name }
Copy link
Member

Choose a reason for hiding this comment

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

Is ObjectSpace available in jruby ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems to be available, but turned off by default.

Since this is only in a test, do you think it is unreasonable to require JRuby users to turn ObjectSpace on for the purposes of running the tests? (i.e., it will not affect anyone using Rails, only those running the tests)

Unfortunately, const_get and const_defined? behave differently between 1.9.3, 2.0, and 2.1, so we cannot use those (if they are even available by default in JRuby -- which I'm not sure of, since they might rely on ObjectSpace for all I know).

Copy link
Member

Choose a reason for hiding this comment

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

we can't have failing jruby test in travis. We either have to turn on ObjectSpace or skip this test in jruby.

cc @senny @jeremy @tenderlove @rafaelfranca

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FWIW, ObjectSpace is used elsewhere in Rails as of the time of this comment. Not a lot, but it is there. Does Rails already require JRuby to have it turned on? Or is the project trying to move away from it?

Alternatively, do you know a way that would work in both? All else being equal, I'd rather have 100% JRuby support without requiring switching from defaults if I can.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, thanks. I'll test it locally on JRuby and see what I can come up with if it fails.

Copy link
Member

Choose a reason for hiding this comment

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

Look good to me, the failure in jruby are unrelated to your change i think.

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, I cannot get the tests to run in JRuby. For me, it says --dev is not a valid option. On travis https://travis-ci.org/rails/rails/jobs/37502973#L223 it is not even loading to run the tests.

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 sure we can think of a way to test this without ObjectSpace. :-/ walking all classes in ObjectSpace seems a bit extreme.

Aaron Patterson
http://tenderlovemaking.com/
I'm on an iPhone so I apologize for top posting.

On Oct 9, 2014, at 9:31 AM, Abdelkader Boudih notifications@github.com wrote:

In activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb:

@@ -883,4 +887,15 @@ def test_redefine_habtm
child.special_projects << SpecialProject.new("name" => "Special Project")
assert child.save, 'child object should be saved'
end
+

  • def test_reflections_do_not_have_nonexistent_constants_as_class_names
  • refute_nil Developer._reflections['shared_computers']
  • Checking the fixture for named association is important here, because it's the only way

  • we've been able to reproduce one of the 2 parts of this bug

  • refute_nil File.read(File.expand_path("../../../fixtures/developers.yml", FILE)).index("shared_computers")
  • Developer._reflections.values.each do |association|
  •  constant = ObjectSpace.each_object(Class).detect{|c| c.to_s == association.class_name }
    
    we can't have failing jruby test in travis. We either have to turn on ObjectSpace or skip this test in jruby.

cc @senny @jeremy @tenderlove @rafaelfranca


Reply to this email directly or view it on GitHub.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tenderlove Because HABTM_* classes are built under the ActiveRecord model's namespace, there are multiple levels we need to check for constants.

In Ruby 1.9.3, const_get does not look recursively if we pass in Developer::HABTM_Projects for instance, while 2.0 mentions "this method will recursively look up constant names if a namespaced class name is provided.

Although I couldn't find it documented, I noticed the same behavior when using const_defined? (which was the reason the original pull request failed on 2.0 and 1.9.3).

We could create our own recursive lookup, but that seems worse to me.

I suppose we could just try accessing the constant under assert_nothing_raised ... I'll try that.

@thedarkone
Copy link
Contributor

ObjectSpace is available in JRuby if used for looking up classes/modules, ie: ObjectSpace.each_object(Module) or ObjectSpace.each_object(Class), but is disabled otherwise:

irb(main):001:0> ObjectSpace.each_object(Module) {|m| }
=> 657
irb(main):002:0> ObjectSpace.each_object(Class) {|m| }
=> 539
irb(main):003:0> ObjectSpace.each_object(Object) {|m| }
RuntimeError: ObjectSpace is disabled; each_object will only work with Class, pass -X+O to enable
    from org/jruby/RubyObjectSpace.java:173:in `each_object'
    from (irb):4:in `evaluate'
    from org/jruby/RubyKernel.java:1121:in `eval'
    from org/jruby/RubyKernel.java:1521:in `loop'
    from org/jruby/RubyKernel.java:1284:in `catch'
    from org/jruby/RubyKernel.java:1284:in `catch'

@codeodor
Copy link
Contributor Author

I just rebased against master since this has sit for so long (2 more betas have come out!)

This should be a show-stopper bug: It breaks documented functionality in at least the case where every test in your Rails app will error out if you use a relationship with a non-standard name, and who knows what else it affects.

@tenderlove you had an objection before to using assert_nothing_raised and asked if we could just use the constant. I don't think that's a good idea.

I don't like assert_nothing_raised but as I see it, we either use that or go back to checking if the constant exists using ObjectSpace.

I'm open to doing that if you prefer that.

Let me know. 😄

# we've been able to reproduce one of the 2 parts of this bug
refute_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers")
Developer._reflections.values.each do |association|
assert_nothing_raised{ association.class_name.constantize }
Copy link
Member

Choose a reason for hiding this comment

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

There's no need to call assert_nothing_raised ; actually it will only yield the content of the given block so we try to avoid to use it. You can simply write association.class_name.constantize inside your #each block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback. I'll remove it, but I preferred it because it makes the intent more clear. I would hate to see the test removed in the future because it doesn't appear to be testing anything. Perhaps the comments are enough.

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 that this is safe ; the test's name is pretty clear and there's always git blame. :-)

@robin850 robin850 added this to the 4.2.0 milestone Nov 2, 2014
@robin850
Copy link
Member

robin850 commented Nov 2, 2014

@codeodor: Could you please add a changelog entry ? Thanks a lot for your patch so far ! :-)

@codeodor
Copy link
Contributor Author

codeodor commented Nov 2, 2014

@robin850 I'll make the suggested changes later today. However since this is simply a bug fix to a beta release, is the changelog entry desired? Normally I would add it, but I thought for something like this it's not wanted. 😄

@robin850
Copy link
Member

robin850 commented Nov 2, 2014

Normally I would add it, but I thought for something like this it's not wanted.

Yep you're right, sorry, nevermind !

@codeodor codeodor force-pushed the fix-17119 branch 2 times, most recently from f284340 to 08b830f Compare November 2, 2014 13:15
@codeodor
Copy link
Contributor Author

codeodor commented Nov 2, 2014

Ok, refute nil changed to assert_not_nil and removed assert_nothing_raised, and rebased.

@tenderlove
Copy link
Member

Is it possible to demonstrate this bug without specifically constantizing the class name?

@tenderlove
Copy link
Member

@tenderlove you had an objection before to using assert_nothing_raised and asked if we could just use the constant. I don't think that's a good idea.

Why? Just do assert_equal "ExpectedClassName", class_name.constantize.name

I still have the earlier question though, is it possible to demo the bug without specifically constantizing that value. This test is testing internals.

@codeodor
Copy link
Contributor Author

codeodor commented Nov 5, 2014

@tenderlove

Why? Just do assert_equal "ExpectedClassName", class_name.constantize.name

Then it would certainly be testing internals. For example, we know one of the classes we want to test is called ModelClass::HABTM_somethings so we could test that as one (there are others we'd want to test as well, because for example when using the fixtures it ignores the class_name option when trying to discover the class based on the relationship name in _reflections).

In any case, suppose the naming convention later changes, or another kind of abstraction is produced as an intermediate layer:

If we iterate over the classes produced by AR, then we cover those cases without tying the test to the internals (other than iterating over the reflections). If we instead hard code the class names in the test, we lose that ability.

Is it possible to demonstrate this bug without specifically constantizing the class name?

I've only thought of 2 ways to do ensure a string is a valid constant:

  1. constantizing its name to raise an error if it is not valid, or
  2. look through the classes in ObjectSpace to see if it exists.

The original version of this test did look at ObjectSpace but was changed to constantize the names. Is there an alternative I haven't considered?

I am totally open to having a better test. I'm not happy with this one. But I was unable to figure out a better way to produce the errors in a test. If you have any more ideas, I would love to improve it. I just feel like hard coding the class names in the test is worse than iterating through _reflections.

My biggest concern is a release of 4.2 without this fix, because I and anyone else who uses a non-standard name as a relationship will be unable to run their tests. For example, with the following line and
a fixture that references the employees relationship:

  has_many :employees, class_name: "User"

every test in the app that loads the fixture will error out.

That is the most immediate problem I encountered in the wild.

@codeodor
Copy link
Contributor Author

codeodor commented Nov 5, 2014

I suppose we could drop the class name part of the test altogether and just ensure the fixture has the expected data that will cause an error without the fix applied, since that's the only place in the wild where I've seen the error.

I only discovered the class name issue as a by-product of that, since it was ignoring the class_name option.

So if you'd like me to just remove that part of the test in an effort to make sure this fix gets into 4.2, I'd be happy to do so.

@tenderlove
Copy link
Member

I suppose we could drop the class name part of the test altogether and just ensure the fixture has the expected data that will cause an error without the fix applied, since that's the only place in the wild where I've seen the error.

Yes, this is exactly what I want. The generated class names, and even constantizing them are internals and I want to be free to change that. In fact, my original implementation created no constants at all for the middle models. Eventually I would like to return to that, but testing directly against the internal constant doesn't really prevent the regression you were seeing. Testing the constants directly forces us to be bound to this implementation.

@codeodor
Copy link
Contributor Author

codeodor commented Nov 9, 2014

Ok, done. Thanks @tenderlove for helping me see the light you were sharing! 😄

tenderlove added a commit that referenced this pull request Nov 9, 2014
Ensure HABTM relationships produce valid class names (Fixes #17119)
@tenderlove tenderlove merged commit 049caa9 into rails:master Nov 9, 2014
@tenderlove
Copy link
Member

Sorry for all the back and forth. Thanks for the hard work on this!!!

@chancancode
Copy link
Member

@tenderlove @codeodor for some reason, this commit broke the build after being merged into master. Would one of you be able to take a look? If not, I'll try to figure it out later this week

@tenderlove
Copy link
Member

Weird. Travis gave it a pass. I can take a look later.

@codeodor
Copy link
Contributor Author

Weird that it didn't catch it before, since the commit that seems to cause the issue was introduced in 2009 so it would have run against it. 😄 (See https://github.com/rails/rails/blame/master/activerecord/test/cases/validations/uniqueness_validation_test.rb#L44)

It seems to only fail when running that test file in isolation, so maybe that's why I didn't catch it.

In any case, if we remove the developers fixture from that test (it is not used), the problem goes away. I will issue a new pull request to fix it.

codeodor added a commit to codeodor/rails that referenced this pull request Nov 10, 2014
@codeodor
Copy link
Contributor Author

See #17578 for the fix.

@codeodor
Copy link
Contributor Author

@tenderlove It was my pleasure. Thanks for having patience with me. 😁

@chancancode
Copy link
Member

Ah, now I know what's going on. @matthewd disabled running the isolated tests on PRs on travis, that's why it didn't caught it :)

rafaelfranca added a commit that referenced this pull request Nov 10, 2014
Fix bug found when running individual tests against #17217 after merging
arunagw added a commit to arunagw/rails that referenced this pull request Nov 14, 2014
`Computer` class needs to be require

See rails#17217 for more details
swanandp pushed a commit to Simplero/rails that referenced this pull request Nov 18, 2014
`Computer` class needs to be require

See rails#17217 for more details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants