Created
November 14, 2011 21:02
-
-
Save eightbitraptor/1365150 to your computer and use it in GitHub Desktop.
A first pass solution for @martinrue's Ordered Jobs Kata
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# started writing the tests for OrderedJobs class | |
# wrote no jobs and a single job, when I got to single job | |
# left that test failing because I realised I needed some | |
# kind of Job, and potentially a parser. | |
# | |
# Started working on a job class, tdd'd it to have a name | |
# and an optional dependancy, and nothing else. | |
# | |
# Still need a way of getting strings into Jobs to make my | |
# single job test case pass though, to keep the OrderedJobs | |
# class just ordering jobs I opt to create a JobsParser and | |
# TDD that. The JobsParser can then pass a list of jobs back | |
# to the OrderedJobs class which will sort them and format | |
# the output | |
# | |
# For the sorting of the output, TDD'd it because I'm not smart | |
# enough to design a big sorting system up front. | |
# | |
# Also using Ruby stdlib Set for an easy way of determining when | |
# al element is in a collection twice. | |
# | |
# In retrospect I think I'd have liked a JobList class or something | |
# like that to handle getting and setting. | |
# | |
# …Other thoughts, in reflection, are that I've probably dissapeared | |
# into Architectural astronaut land. If @cwninja where here he'd have | |
# taken my keyboard away by now. | |
require 'set' | |
class OrderedJobs | |
def self.process(jobs_spec) | |
@joblist = JobParser.parse(jobs_spec) | |
sort!(JobParser.parse(jobs_spec)) | |
end | |
def self.cyclic_dependancy_in(jobs) | |
jobs.each do |job_name, job| | |
dep_chain = Set.new | |
while(job = jobs[job.dependancy]) | |
break if !job.dependancy | |
if dep_chain.add?(job.name).nil? | |
return true | |
end | |
end | |
end | |
return false | |
end | |
def self.sort!(jobs) | |
raise(StandardError) if cyclic_dependancy_in(jobs) | |
jobs.values.inject([]) { |sorted_jobs, job| | |
if !sorted_jobs.include? job.name | |
sorted_jobs << job.name | |
end | |
if job.dependancy | |
sorted_jobs.delete(job.dependancy) | |
job_pos = sorted_jobs.index(job.name) | |
sorted_jobs.insert(job_pos, job.dependancy) | |
end | |
sorted_jobs | |
} | |
end | |
end | |
class Job | |
attr_accessor :name, :dependancy | |
def initialize(name, dep=nil) | |
if dep == name | |
raise ArgumentError.new("Jobs cannot depend on themselves") | |
end | |
@name = name | |
@dependancy = (dep == "") ? nil : dep | |
end | |
def dependancy? | |
!!@dependancy | |
end | |
end | |
class JobParser | |
def self.parse(job_spec) | |
job_spec.split("\n").inject(Hash.new){ |acc, spec_line| | |
j = Job.new(*spec_line.split('=>').map(&:strip)) | |
acc[j.name] = j | |
acc | |
} | |
end | |
end | |
require 'rspec' | |
describe JobParser do | |
let(:parsed_job) { JobParser.parse("a => b") } | |
context "when given a single job" do | |
it "indexes the collection by the jobname" do | |
parsed_job.keys.should include("a") | |
parsed_job.keys.length.should be(1) | |
end | |
it "stores jobs in the collection" do | |
parsed_job["a"].should be_a(Job) | |
end | |
it "names the job correctly" do | |
parsed_job["a"].name.should eq("a") | |
end | |
it "sets up the correct dependancy" do | |
parsed_job["a"].dependancy.should eq("b") | |
end | |
end | |
context "with multiple jobs" do | |
let(:parsed_jobs){ JobParser.parse("a => b | |
b => | |
c => ")} | |
it "creates the correct number of jobs" do | |
parsed_jobs.length.should be(3) | |
end | |
it "parses the args correctly" do | |
parsed_jobs.values.map(&:name).should =~ ["a", "b", "c"] | |
end | |
end | |
end | |
describe Job do | |
it "is invalid without a name" do | |
expect{ | |
Job.new | |
}.to raise_error(ArgumentError) | |
end | |
it "cannot depend on itself" do | |
expect{ | |
Job.new("a", "a") | |
}.to raise_error(ArgumentError) | |
end | |
it "returns it's name when asked" do | |
Job.new(:test).name.should be :test | |
end | |
context "without a dependancy" do | |
it "returns nil dependancy" do | |
Job.new(:test).dependancy.should be_nil | |
end | |
end | |
context "when a dependancy is supplied" do | |
it "returns the dependancy when asked" do | |
Job.new(:test, :dep).dependancy.should eq(:dep) | |
end | |
end | |
end | |
describe OrderedJobs do | |
context "When I pass no jobs" do | |
it "returns an empty collection" do | |
OrderedJobs.process("").should eq([]) | |
end | |
end | |
context "when I pass a single job" do | |
it "returns the job to be run" do | |
OrderedJobs.process("a => ").should eq(["a"]) | |
end | |
end | |
context "multiple jobs, single dependancy" do | |
let(:job_spec){ "a => | |
b => c | |
c =>" } | |
it "puts c before b" do | |
jobs = OrderedJobs.process(job_spec) | |
jobs.join.should =~ /c.*b/ | |
end | |
end | |
context "multiple jobs, multiple dependancies" do | |
let(:jobs){ OrderedJobs.process("a => | |
b => c | |
c => f | |
d => a | |
e => b | |
f => ")} | |
it "puts f before c" do | |
jobs.join.should =~ /f.*c/ | |
end | |
it "puts c before b" do | |
jobs.join.should =~ /c.*b/ | |
end | |
it "puts b before e" do | |
jobs.join.should =~ /b.*e/ | |
end | |
it "puts a before d" do | |
jobs.join.should =~ /a.*d/ | |
end | |
it "contains all six letters" do | |
jobs.length.should be(6) | |
end | |
end | |
context "self referential jobs" do | |
let(:job_spec){ "a => | |
b => c | |
c => f | |
d => a | |
e => | |
f => b"} | |
it "raises an Error" do | |
expect { | |
OrderedJobs.process(job_spec) | |
}.to raise_error(StandardError) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment