Get a copy of IronRuby

You can download the latest IronRuby release from: IronRuby.net / Get IronRuby

Get yourself a mini IronRuby command line environment set up

  • Decompress the Zip file somewhere
  • Create batch files to start a command prompt with all the IronRuby paths. Place them in the bin directory (where iirb.exe lives):

bin/ruby-prompt.bat

%comspec% /k %cd%\settings.bat

bin/settings.bat

set PATH=%CD%;%cd%\..\lib\IronRuby\gems\1.8\bin;%PATH%

Great! Now when you click on ruby-prompt.bat you will get a command prompt that can exec iirb, igem and the rest of the IronRuby commands.

Get rspec

(in your IronRuby command prompt run)

igem install rspec

Write your tests

In this example I am testing an “LRU Cache”:Cache replacement policies - Wikipedia

require File.dirname(__FILE__) + '\..\LRUCache\bin\Debug\LRUCache.dll'
include System::Collections::Generic
include System


# mostly by the sadly missing _why 
class Object
  def metaclass; class << self; self; end; end
  def meta_eval &blk; metaclass.instance_eval &blk; end

  def meta_alias(new, old) 
    meta_eval { alias_method new, old}
  end
  
  def meta_def(name, &blk)
    meta_eval { define_method name, &blk }
  end

end

describe "LRUCache" do 
  
  def create_cache(capacity)
    cache = LRUCache::LRUCache.of(System::String,System::String).new(capacity)
    [
      [:try_get, :TryGetValue], 
      [:contains_key?, :ContainsKey], 
      [:add, :Add],
      [:remove, :Remove],
      [:count, :Count]
    ].each { |new,old| cache.meta_alias(new, old) }
    
    cache.meta_def(:replay) do |array|
      array.each do |key,val| 
        if val.nil?; cache[key]; else; cache[key] = val; end  
      end
      cache
    end
    
    cache
  end

  it "should never exceed its capacity" do 
    cache = create_cache(10) 
    (11).times { |i|
      cache[i.to_s] = "data" 
    }
    cache.count.should == 10
  end

  it "should throw an exception if an item is accessed via the index "\
    "and its not there" do 
    cache = create_cache(10)
    lambda { cache["bla"] }.should raise_error(KeyNotFoundException)
  end

  it "should expire stuff that was not recently used, when capacity is reached" do 
    cache = create_cache(3)
    cache.replay(
      [ 
        ["a","aa"],
        ["b","bb"],
        ["c","cc"],
        "a",
        "b",
        ["d","dd"]
      ]
    )
    
    cache.contains_key?("c").should == false
    ["a","b","d"].each{|key| cache.contains_key?(key).should be_true }
  end

  it "should increase the count when stuff is added" do 
    cache = create_cache(3)
    lambda { cache.add("a","aa") }.should change(cache, :count).by(1) 
  end

  it "should decrease the count when stuff is removed" do 
    cache = create_cache(3) 
    cache.add("a", "aa") 
    lambda { cache.remove("a") }.should change(cache, :count).by(-1) 
  end

  it "should throw if a cache is initialized with 0 capacity" do 
    lambda { create_cache(0) }.should raise_error(ArgumentException) 
  end

  it "should allow us to enumerate through the items" do 
    input = [["a","aa"],["b","bb"], ["c","cc"]]
    
    cache = create_cache(3).replay(input)
    data = [] 
    cache.each do |pair|
      data << [pair.Key, pair.Value]
    end
    data.should == input 
  end

  describe "(try get)" do 
    before :each do 
      @cache = create_cache(3) 
    end

    it "should support missing items" do 
      found, value = @cache.try_get("a")
      found.should == false
    end
    
    it "should support existing items" do 
      @cache["a"] = "aa"
      found, value = @cache.try_get("a")
      found.should == true
      value.should == "aa" 
    end
  end
end

Write the C# classes to power the LRUCache

IndexedLinkedList.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LRUCache {
    public class IndexedLinkedList<T> {

        LinkedList<T> data = new LinkedList<T>();
        Dictionary<T, LinkedListNode<T>> index = new Dictionary<T, LinkedListNode<T>>();

        public void Add(T value) {
            index[value] = data.AddLast(value);
        }

        public void RemoveFirst() {
            index.Remove(data.First.Value);
            data.RemoveFirst();
        }

        public void Remove(T value) {
            LinkedListNode<T> node;
            if (index.TryGetValue(value, out node)) {
                data.Remove(node);
                index.Remove(value);
            }
        }

        public int Count {
            get {
                return data.Count;
            }
        }

        public void Clear() {
            data.Clear();
            index.Clear();
        }

        public T First {
            get {
                return data.First.Value;
            }
        }
    }
}</code></pre> 

LRUCache.cs 

<pre><code>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LRUCache {
    public class LRUCache<TKey, TValue> : IDictionary<TKey, TValue> {

        Dictionary<TKey, TValue> data;
        IndexedLinkedList<TKey> lruList = new IndexedLinkedList<TKey>();
        ICollection<KeyValuePair<TKey, TValue>> dataAsCollection;
        int capacity;

        public LRUCache(int capacity) {

            if (capacity <= 0) {
                throw new ArgumentException("capacity should always be bigger than 0");
            }

            data = new Dictionary<TKey, TValue>(capacity);
            dataAsCollection = data;
            this.capacity = capacity;
        }

        public void Add(TKey key, TValue value) {
            if (!ContainsKey(key)) {
                this[key] = value;
            } else {
                throw new ArgumentException("An attempt was made to insert a duplicate key in the cache.");
            }
        }

        public bool ContainsKey(TKey key) {
            return data.ContainsKey(key);
        }

        public ICollection<TKey> Keys {
            get {
                return data.Keys;
            }
        }

        public bool Remove(TKey key) {
            bool existed = data.Remove(key);
            lruList.Remove(key);
            return existed;
        }

        public bool TryGetValue(TKey key, out TValue value) {
            return data.TryGetValue(key, out value);
        }

        public ICollection<TValue> Values {
            get { return data.Values; }
        }

        public TValue this[TKey key] {
            get {
                var value = data[key];
                lruList.Remove(key);
                lruList.Add(key);
                return value;
            }
            set {
                data[key] = value;
                lruList.Remove(key);
                lruList.Add(key);

                if (data.Count > capacity) {
                    Remove(lruList.First);
                    lruList.RemoveFirst();
                }
            }
        }

        public void Add(KeyValuePair<TKey, TValue> item) {
            Add(item.Key, item.Value);
        }

        public void Clear() {
            data.Clear();
            lruList.Clear();
        }

        public bool Contains(KeyValuePair<TKey, TValue> item) {
            return dataAsCollection.Contains(item);
        }

        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
            dataAsCollection.CopyTo(array, arrayIndex);
        }

        public int Count {
            get { return data.Count; }
        }

        public bool IsReadOnly {
            get { return false; }
        }

        public bool Remove(KeyValuePair<TKey, TValue> item) {

            bool removed = dataAsCollection.Remove(item);
            if (removed) {
                lruList.Remove(item.Key);
            }
            return removed;
        }


        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
            return dataAsCollection.GetEnumerator();
        }


        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            return ((System.Collections.IEnumerable)data).GetEnumerator();
        }

    }
}

Run your tests:

C:\Users\sam\Desktop\Source\LRUCache\spec>spec lru_spec.rb
.........

Finished in 0.3340191 seconds

9 examples, 0 failures

Yay, we have a working rspec test suite.

I know, there is a lot to chew on here, in future posts I will try to explain some of the trickery that is going on.

Comments

Scott_Bellware over 14 years ago
Scott_Bellware

Are you trying to teach how to implement the cache, or introduce people to RSpec? If you're trying to introduce people to RSpec, you might consider using an example that isn't too geeky, as the example will distract from the attention that you're trying to call to RSpec.

Sam Saffron over 14 years ago
Sam Saffron

I left this really geeky, mainly as a teaser, it does cover a ton of rspec functionality, notice how its not too liberal with the use of setup methods.

I plan to decompose bits of it in a couple of future posts, like the remapping of methods (so methods look more rubyish), explain the lambdas and the exception expectations, nesting and so on.

Sam Saffron over 14 years ago
Sam Saffron

Note, for those wanting mocking support have a look at http://github.com/casualjim/caricature/tree/master

Paul_Rayner over 14 years ago
Paul_Rayner

Love the example Sam! Shouldn't bin/ruby-prompt.bat be:

comspec /k %CD%\settings.bat

Paul.

Sam Saffron over 14 years ago
Sam Saffron

Thanks Paul!!!

I’ll fix that up, my textile processor is swallowing it…

Paul_Rayner over 14 years ago
Paul_Rayner

ok. How do I edit my comment? I put percentage signs around the CD, but the blog engine has stripped them so my comment is wrong.

Sam Saffron over 14 years ago
Sam Saffron

I need to implement comment edits … its one missing piece of my blog.


comments powered by Discourse