skip to main | skip to sidebar
JJinuxLand
This is a purely technical blog concerning topics such as Python, Ruby, Linux, open source software, the Web, and lesser-known programming languages.
Ad maiorem Dei gloriam inque hominum salutem.
Thursday, August 13, 2009
Rails: Dynamic 404s, authlogic, Cucumber, and rescue_from
I'm using Rails, Cucumber, and authlogic. I want my 404 pages to be rendered dynamically; that way, I can use my application-wide layout, which is a lot friendlier for lost users. I want ActiveRecord::RecordNotFound exceptions to be handled by the dynamic 404 page. I want Cucumber tests to verify that everything is working correctly.
Getting everything working together at the same time turned out to be extremely challenging. Cucumber made it hard for me to test my ActiveRecord::RecordNotFound handling. authlogic made it impossible for me to catch ActionController::RoutingError exceptions. Defining ApplicationController#render_optional_error_file as described here conflicted with authlogic and/or Cucumber. After several hours, I finally got it all working. Here's how:
First, I added the following to ApplicationController:
# Note, I can't use rescue_from to catch ActionController::RoutingError,# otherwise authlogic breaks. Hence, I set a default route instead.rescue_from ActiveRecord::RecordNotFound, :with => :render_404...# I'm handling 404s manually so that I can use the application-wide layout.def render_404 render :template => "errors/error_404", :status => "404 Not Found"endNote, render_404 must be a public method.
At the bottom of routes.rb, I have:
# If all else fails, render a 404.map.connect '*path', :controller => :application, :action => :render_404In app/views/errors/error_404.html.erb, I have:
Sorry, the page you were looking for does not exist.That takes care of rendering 404s if no routes match or if there is an ActiveRecord::RecordNotFound exception.
My Cucumber tests look something like:
Scenario: there should be a custom 404 page for routing errors Given I am on the homepage And I am simulating a remote request When I am on an invalid URL Then I should get a "404 Not Found" response And I should see "Sorry, the page you were looking for does not exist." And I should see "Log In"Scenario: there should be a custom 404 page for record not found errors Given I am logged in as admin And I am simulating a remote request When I am on an invalid user Then I should get a "404 Not Found" response And I should see "Sorry, the page you were looking for does not exist.In features/step_definitions/helper_steps.rb, I have:
def assert_response_status(http_status, message) response.status.should == "#{http_status} #{message}"endThen /^I should get a "(\d+) ([^"]+)" response$/ do |http_status, message| assert_response_status(http_status, message)endGiven /^I am simulating a remote request$/ do header "REMOTE-ADDR", "10.0.1.1"endNote, the REMOTE-ADDR header is necessary or else Rails will render a normal exception page when you're trying to test the 404 page.
In features/support/env.rb b/features/support/env.rb I have:
# Comment out the next line if you want Rails' own error handling# (e.g. rescue_action_in_public / rescue_responses / rescue_from)## I'm commenting it out in order to test that my 404 handling (using# rescue_from) works properly.# Cucumber::Rails.bypass_rescueFiguring that out took the longest. Cucumber was monkey patching Rails behind my back. I spent a few hours trying to figure out why my Rails rescue_from clause wasn't running when it turned out that Cucumber was to blame.
In features/support/paths.rb, I have these paths defined:
when /an invalid URL/ "/totally_bogus_dude"when /an invalid user/ user_path(-1)Finally, I added the following to public/404.html:
<!-- In theory, this file should no longer be necessary since I'm handling 404s dynamically. I'm going to leave it here in case something breaks behind my back.-->Whew! That was rough!
Posted by Shannon -jj Behrens at 8:52 PM 0 comments
Labels: rails, ruby, testing, web
Wednesday, August 12, 2009
Rails: authlogic Code Review
I just spent a couple days code reviewing authlogic. First of all, let me say it's good code. Usually, I have a lot to say when I code review someone's code, but this time, I was pretty happy. Here are the corrections I submitted.
Clearly, the author of authlogic knows Ruby a heck of a lot better than I do; I'm still relatively new to Ruby. There were a bunch of idioms, language features, and design decisions that caught my attention as a Python programmer. That's what this blog post is about.
The first thing I noticed is that the code pieces together huge classes by mixing in tons of modules. For instance, in authlogic/session/base.rb:
module Authlogic module Session # This is the base class Authlogic, where all modules are included. For # information on functionality see the various sub modules. class Base include Foundation include Callbacks include Timeout include Params include Cookies include Session include HttpAuth ...There are 22 included modules in all.
Here is one of the included modules, authlogic/session/foundation.rb:
module Authlogic module Session module Foundation def self.included(klass) klass.class_eval do extend ClassMethods include InstanceMethods end end module ClassMethods ... end module InstanceMethods ... end end endendThe "included" method gets called when the module is included. It uses "class_eval" to explicitly mix in some methods. Notice that the "ClassMethods" module is explicitly mixed in using "extend", and the "InstanceMethods" module is explicitly mixed in using "include".
I had to refer to my Ruby book to figure out the difference. A class includes a module when it wants to mix in the module's functions as instance methods. It affects every instance of the class. An object extends a module when it wants to mix in the module's functions just into itself. In this case, the object in question is a class, so it's in essence mixing in functions as class methods. This is a vivid reminder that unlike languages like C++, classes in Ruby are objects too.
By the way, I do worry that this excessive use of mixins will lead to namespace conflicts. I ran self.methods.size in my UserSession class, which inherits from Authlogic::Session::Base. It reported 271 methods. I did the same thing in my User model, which inherits from ActiveRecord::Base. It reported 550 methods!
The next thing I noticed was methods like:
def session_ids self.class.session_idsenddef session_class self.class.session_classendIn Python, if you call a method on an instance, if it can't find the method among the instance methods, it'll also look at the class methods. Java does this too, although it's frowned upon. Ruby doesn't look among the class methods when you call an instance method. Hence, the code is explicitly delegating to the class in this code. Code like the above happens a surprisingly large number of times across the code base. I'm surprised there isn't a helper like "delegate_to_class :only => [:session_ids, :session_class]".
Another thing I noticed is code like this:
module Callbacks METHODS = [ "before_password_set", "after_password_set", "before_password_verification", "after_password_verification" ] ... private METHODS.each do |method| class_eval <<-"end_eval", __FILE__, __LINE__ def #{method} run_callbacks(:#{method}) { |result, object| result == false } end end_eval endendLook closely at '<<-"end_eval", __FILE__, __LINE__...end_eval'. That's actually evaluating a "heredoc" (or at least that's what they call it in other languages) in order to define a method. Using some flavor of "eval" to add methods on the fly is fairly common in Ruby.
Another thing I noticed is code like:
def find_using_perishable_token(token, age = self.perishable_token_valid_for)In Python, the defaults to a function are evaluated once, as the function is defined. They are not evaluated every time the function is called. If you forget that Python has "static defaults", you'll eventually get bitten by a bug. Apparently, that's not the case in Ruby:
irb(main):001:0> def f(default = [])irb(main):002:1> default << "hi"irb(main):003:1> end=> nilirb(main):004:0> f=> ["hi"]irb(main):005:0> f=> ["hi"]authlogic has impressively good docstrings. Ruby uses comments for docstrings. Python uses specially-placed strings. Hence, in Python, you can piece together docstrings at runtime using string interpolation, etc. I often make good use of this to keep my docstrings DRY and to prevent them from going stale. For instance, I might put a comment in a variable, and reuse that same comment in multiple docstrings. The code for authlogic occasionally has to duplicate the same comment.
Ruby programmers tend to use longer function names and they don't often try to limit their code to 80 columns. Here is a mildly comical case:
# A convenience function to merge options into the validates_length_of_login_field_options. So intead of:## self.validates_length_of_password_field_options = validates_length_of_password_field_options.merge(:my_option => my_value)## You can do this:## merge_validates_length_of_password_field_options :my_option => my_valuedef merge_validates_length_of_password_field_options(options = {}) self.validates_length_of_password_field_options = validates_length_of_password_field_options.merge(options)endI enjoyed the metaprogramming in authlogic. For instance, code like the following is fairly common across the codebase:
self.class.send(:attr_writer, login_field) if !respond_to?("#{login_field}=")The last thing that caught me off guard was that Ruby supports:
1/0 rescue 'hi'Apparently, that's an expression-level form of begin/rescue (aka try/finally). The above evaluates to "hi".
Anyway, as I said, authlogic is good code, and I learned a lot :)
Posted by Shannon -jj Behrens at 9:31 PM 0 comments
Labels: python, rails, ruby, web
Saturday, August 08, 2009
Vim: Transparency in MacVim
Wahoo! MacVim now supports transparency. Finally, I can have a nice, transparent Vim window to match my transparent terminal, and I don't have to give up all the modern conveniences. If you use the "Homebrew" profile for your Terminal.app, try putting the following in your .gvimrc:
colorscheme torteset transparency=15Posted by Shannon -jj Behrens at 9:29 PM 0 comments
Labels: apple, vim
Open Source: Closed Source Video Games
About a year ago, my buddy Ben Bangert gave us his old GameCube. My wife and I really enjoyed playing Paper Mario: The Thousand-Year Door.
"Paper Mario" is a closed source game. I bought it used at my local GameStop. Since I'm a bit of a free software nut, you might wonder how I could live with the thought of playing a closed source game. The fact of the matter is, I don't think "Paper Mario" could be produced in an open source manner. Technically, I'm sure it could, but who would want to? A hundred people or more were involved in producing that game, and what do they get out of it? I wouldn't volunteer on such a project--I have real work to do! If I can't imagine volunteering my time on such a project, then why would I expect anyone else to? Instead, I paid money. I'm happy because I have a fun game to play. The developers are happy because they got paid. Believe it or not, it's a win win situation.
However, I do have enough time to write a small video game. I wrote it in a week with my buddy Adam Ulvi. It's open source. It's based on the open source library, PyGame. I also made use of an open source library called PGU aka "Phil's Game Utilities". Phil wrote PGU while he was writing his own video games. The fact that PyGame is open source has saved me multiple times, since I've often looked at the source code in a pinch.
Now, I'm not trying to say that big projects can't be produced in an open source way. If that were true, Linux wouldn't exist. I'm just saying that I enjoy large closed source games like "Paper Mario: The Thousand-Year Door", and I also enjoy writing small open source games using open source tools like PyGame. My point is, there's room in this world for both!
On a more personal note, I'm coming to the conclusion that I need to just relax. The industry is going to go in whichever direction it's going to go, and there ain't a heck of a lot I can do about it. My best bet is to stay flexible, friendly, and helpful--as usual ;)
Posted by Shannon -jj Behrens at 3:00 AM 0 comments
Labels: open source, pygame, python, video games
Thursday, August 06, 2009
Testing: How SQLite is Tested
Alex Martelli sent me this: How SQLite Is Tested. It gave me a new respect for both SQLite and extremely-thorough testing. For instance, "the project has 709 times as much test code and test scripts" as actual source code.
Posted by Shannon -jj Behrens at 11:11 PM 0 comments
Labels: testing
Wednesday, August 05, 2009
Linux: OpenGEU
Continuing from my post yesterday, Linux: Open Source and my MacBook, I gave OpenGEU 8.10 a shot under VMware Fusion on my MacBook.
OpenGEU is an Ubuntu-based Linux distribution that uses E17 (aka the unstable version of Enlightenment). The project was started and designed by the Italian artist Luca D.M. (aka TheDarkMaster).
In short, it isn't quite as "cohesive" from a feature point of view as stock Ubuntu, but it's stunningly beautiful and performs a lot better. I gave VMware 780MB of RAM, but it's currently using only half that. It also seems to use a lot less CPU. It's actually tolerable under VMware.
Here are some problems I encountered and the solutions I came up with:
Don't use "Linux Easy Install". VMware claims that it can install the ISO automatically since it's based on Ubuntu. However, this just lead to a blank screen for me.
I told the installer to log me in automatically. Why not? I already have to authenticate with my Mac. This makes booting seem a lot faster.
I had to forcibly restart the virtual machine after installation. It just hung. VMware has done this to me before.
VMware-tools installed just fine.
I had some serious dpi problems. This resulted in very large fonts. Thanks to this wonderful page, I was able to fix the problem. I was very happy about that since this problem has always stumped me in the past. I edited /etc/X11/xorg.conf and added the following to the Monitor section, "DisplaySize 338 211 # 1280x800 96dpi".
I couldn't figure out how to move among the different virtual desktops. Alt-shift-(left|right|up|down) does the trick. That's good, because I use Cntl-(left|right|up|down) to move among my different spaces on the Mac. Hence, there's no conflict.
I tried sharing folders between my host and guest operating system. This worked amazingly well. You can enable it on the fly, aka without rebooting. /mnt/hgfs shows up on the guest OS. The only problem is the same problem you typically get with NFS. If your user ID and group ID are different on your host operating system, the files won't match your username and groupname on your guest operating system. It's possible to hack your away around this problem, but it's still a pain.
I told VMware to "pass power status to the virtual machine", but this led to warnings from OpenGEU.
Since OpenGEU doesn't use much from GNOME, it doesn't run startup tasks. Hence, I had to manually run /usr/bin/vmware-user every time I logged in to let VMware do its thing and fix the resolution. To add insult to injury, this causes one of the panels to end up in the middle of the screen. However, you can right-click on the very edge of it and tell it to restart. This fixes the problem.
As beautiful as OpenGEU is, it still made me miss the fonts on my Mac. I guess I'm spoiled. Furthermore, the screen seems "grainy", and the colors aren't quite perfect. I always complain about that in Linux. I have no clue why Linux running with the right resolution on my Mac under VMware looks slightly off, but normal OS X doesn't. Maybe it has to do with finely tuned gamma correction or anti-aliased graphics or something, I don't know. However, it kind of messes with my obsessive compulsive nature.
Anyway, I want to thank all the Python guys for their comments on my blog yesterday. They made me feel a lot better ;)
Posted by Shannon -jj Behrens at 3:49 AM 2 comments
Labels: apple, Linux, python, ubuntu
Tuesday, August 04, 2009
Rails: Cache Inconsistency Caused by a Common Rails Idiom
Rails has a feature called "flash". Anything put in the flash is available on the next page load. It's based on the session, but it goes away after the next page load. It's perfect for saving a message for the user even if you're going to do a redirect.
Rails also has a function called reset_session that wipes the user's session and gives him a new one. Agile Web Development with Rails says you should call the reset_session method after the user logs out of your site. This helps avoid session fixation attacks.
Unfortunately, authlogic doesn't do this automatically. Hence, I decided to do it myself. I had code like:
reset_sessionflash[:notice] = "Log out successful!"The code works, but the message "Log out successful!" doesn't show up. Fortunately, my tests caught that. It turns out that Rails has a known bug that if you call reset_session, flash breaks. Why?
Rails uses an idiom that looks like:
def foo @foo ||= calculate_fooendThis idiom implicitly uses @foo as a cache so that calculate_foo is only called the first time the foo method is called.
The great thing about caches is that they can prevent unnecessary, time-consuming work. The bad thing about them is that you have to deal with cache inconsistency problems.
flash uses this idiom:
def flash unless defined? @_flash @_flash = session["flash"] ||= FlashHash.new @_flash.sweep end @_flashendSo does the session. You might see where I'm going with this.
When you call reset_session, there are two caches that have become inconsistent, and the fact that there are multiple layers of cache inconsistency is what lead to the bug.
Posted by Shannon -jj Behrens at 5:41 PM 0 comments
Labels: rails, ruby, web
Linux: Open Source and my MacBook
I have a love hate relationship with my MacBook. I adore the little bugger. I love how everything just works. I like the screen. I like the software. However, I really don't have much love for Apple, the company. I've been getting the itch to edge a little bit more toward the open source side. This should not be surprising since I'm a Linux zealot at heart.
One thing that was a real turnoff for me is that Apple stopped releasing binary installers for standalone Darwin. The PureDarwin project is hoping to pickup where OpenDarwin left off, however, it's obviously tough going. I'll keep my eye on it, but I don't think it'll be production-ready any time soon.
I've given up, to some degree, on VMware and VirtualBox. They're useful, but they're not the sort of thing you want to live in on a day-to-day basis. They're just not fast enough. They're better for occasional use.
I was really intrigued to discover Elive. Some guy took Debian and Enlightenment 17, tweaked it out for a Mac, and then released it as an ISO. Interestingly, he asks, rather forcefully, for a donation before you can download the ISO. I've heard that the system is absolutely beautiful, but since it's mostly a one-man show, it's obviously not perfect.
The documentation for getting Ubuntu to run on a MacBook is definitely not a one-man show. It's amazing how well they've documented everything. Don't get me wrong--it's still a total pain in the rear getting everything working just right, but still--they're to be commended.
In order to dual-boot Linux, you need to install rEFIt, which is basically a bootloader. It installed quite easily for me. You also have to have some room for a Linux partition. In theory, Apple's Disk Utility and BootCamp can both resize your existing partition in order to make room for another operating system. As you might imagine, this doesn't work out so well in practice.
I was immediately impressed when I heard that those tools could resize a partition. I thought to myself, boy, they sure must be smart if they can move files around in the partition in order to make room at the end of it. Unfortunately, I was too right. If you've been using your Mac for a while, Disk Utility will report "no space left on device" when you try to resize the partition, even if you have plenty of space. That's because the drive will be a bit fragmented, and it doesn't know how to move the fragments around. Apparently, this is a super common problem. However, it's not so common that Apple will admit to it in their documentation.
There are three solutions. One is to copy your system to a second disk and then copy it back. This basically defrags the partition. The second is to buy a $100 defrag tool. I'm broke right now, so neither of those are acceptable options. The third is to just wipe your hard drive and start from scratch. Ah, I fondly remember the days in college when I had time to do things like that ;)
I think I'm just stuck. I don't have another 12 hours to mess with this, and I'm short on funds to just "make the problem go away." I have this wonderful machine that I love, but, as a lot of Linux enthusiasts will say, it's like wearing a pair of golden handcuffs.
I love how well suspend works, how well the wireless works, how long my battery lasts (almost 5 hours!), and how nice my screen is (I really don't understand why it's so much nicer than every Dell I've ever owned). However, I think I'd like to find a way to make my Mac even more open source. I'm already running MacPorts, and I use open source alternatives for just about everything. (By the way, I must admit that I paid for VMware Fusion and for The Missing Sync.) Perhaps it's just my OCD talking, but I want more!
So I spent two days trying to make my Mac more open source, and I didn't really get anywhere. Hopefully this blog post is helpful somehow to someone. If any of you out there in the Python world have helpful suggestions, I'm all ears :-D
Posted by Shannon -jj Behrens at 12:15 AM 11 comments
Labels: apple, darwin, Linux, OS X, python
Saturday, August 01, 2009
Rails: Foreign Key Constraints
I like referential integrity. Rails doesn't provide support for database-level foreign key constraints. "Agile Web Development with Rails" uses code such as the following to add foreign key constraints:
t.integer :product_id, :null => false, :options => "CONSTRAINT fk_line_item_products REFERENCES products(id)"That doesn't work for me. The migration ran just fine, but when I looked at the database, the constraint wasn't present. I hate it when things fail silently!
I tried a plugin to add foreign key constraints, but I got frustrated pretty quickly. I fell back to executing plain SQL:
execute %{ CREATE TABLE roles_users ( role_id INT NOT NULL, user_id INT NOT NULL, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX (role_id), INDEX (user_id) ) ENGINE = INNODB}The nice thing about plain SQL is that I can understand exactly what's going on. There are no layers of abstraction in my way, and if my SQL is wrong, MySQL will complain.
To be fair, MySQL's errors aren't always the most helpful. I encountered a couple of syntax errors. One was because I misspelled a column name. Another was because I put the name of the foreign key constraint in the wrong place. In both cases, I ended up with the following:
ERROR 1025 (HY000): Error on rename of './myapp_development/mymodel' to './myapp_development/#sql2-87-40c' (errno: 152)Lovely! Oh well, at least it's better than failing silently.
Since I'm trying out test driven development, I knew I had to write a test before I could go mucking around with my schema. It turned out to be fairly hard to test that the database itself enforces a foreign key constraint. After all, Rails loves to try to enforce those constraints itself. Here's the RSpec test I ended up with:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')describe Role do context "the roles table" do it "should enforce foreign key constraints in the database" do verbose = ActiveRecord::Migration.verbose begin ActiveRecord::Migration.verbose = false result = ActiveRecord::Migration.execute("SHOW CREATE TABLE roles_users") sql = result.fetch_row[1] [/CONSTRAINT/, /FOREIGN KEY/, /REFERENCES/].each do |phrase| sql.should =~ phrase end ensure ActiveRecord::Migration.verbose = verbose end end endendUgh, painful. (Apparently, the silence method is only available on controllers. There's a different logger for migrations.)
Oh well. My tests pass, and I have referential integrity.
Posted by Shannon -jj Behrens at 1:40 AM 1 comments
Labels: MySQL, rails, ruby, testing, web
Older Posts Subscribe to: Posts (Atom) About Me
Shannon -jj Behrens
Concord, California, United States