├── .document ├── .gitignore ├── LICENSE ├── README.rdoc ├── Rakefile ├── lib └── cowsay.rb └── spec ├── cowsay_spec.rb ├── spec.opts └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Avdi Grimm 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = cowsay 2 | 3 | Demo code for the talk "Confident Code". An illustration of how not to write 4 | code, using the example of an interface to the "cowsay" utility. 5 | 6 | == Copyright 7 | 8 | Copyright (c) 2009 Avdi Grimm. See LICENSE for details. 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "cowsay" 8 | gem.summary = %Q{TODO: one-line summary of your gem} 9 | gem.description = %Q{TODO: longer description of your gem} 10 | gem.email = "avdi@avdi.org" 11 | gem.homepage = "http://github.com/avdi/cowsay" 12 | gem.authors = ["Avdi Grimm"] 13 | gem.add_development_dependency "rspec", ">= 1.2.9" 14 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 15 | end 16 | Jeweler::GemcutterTasks.new 17 | rescue LoadError 18 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 19 | end 20 | 21 | require 'spec/rake/spectask' 22 | Spec::Rake::SpecTask.new(:spec) do |spec| 23 | spec.libs << 'lib' << 'spec' 24 | spec.spec_files = FileList['spec/**/*_spec.rb'] 25 | end 26 | 27 | Spec::Rake::SpecTask.new(:rcov) do |spec| 28 | spec.libs << 'lib' << 'spec' 29 | spec.pattern = 'spec/**/*_spec.rb' 30 | spec.rcov = true 31 | end 32 | 33 | task :spec => :check_dependencies 34 | 35 | task :default => :spec 36 | 37 | require 'rake/rdoctask' 38 | Rake::RDocTask.new do |rdoc| 39 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 40 | 41 | rdoc.rdoc_dir = 'rdoc' 42 | rdoc.title = "cowsay #{version}" 43 | rdoc.rdoc_files.include('README*') 44 | rdoc.rdoc_files.include('lib/**/*.rb') 45 | end 46 | -------------------------------------------------------------------------------- /lib/cowsay.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'logger' 3 | require 'delegate' 4 | 5 | class NullObject 6 | def initialize 7 | @origin = caller.first 8 | end 9 | 10 | def __null_origin__ 11 | @origin 12 | end 13 | 14 | def method_missing(*args, &block) 15 | self 16 | end 17 | 18 | def nil? 19 | true 20 | end 21 | end 22 | 23 | def Maybe(value) 24 | value.nil? ? NullObject.new : value 25 | end 26 | 27 | module Cowsay 28 | class WithPath < SimpleDelegator 29 | def path 30 | case __getobj__ 31 | when File then super 32 | when nil then "return value" 33 | else inspect 34 | end 35 | end 36 | end 37 | 38 | class Cow 39 | def initialize(options={}) 40 | @io_class = options.fetch(:io_class){IO} 41 | @logger = options.fetch(:logger){Logger.new($stderr)} 42 | end 43 | 44 | def say(message, options={}) 45 | return "" if message.nil? 46 | options[:cowfile] and assert(options[:cowfile].to_s !~ /^\s*$/) 47 | 48 | width = options.fetch(:width) {40} 49 | eyes = Maybe(options[:strings])[:eyes] 50 | cowfile = options[:cowfile] 51 | destination = WithPath.new(options[:out]).path 52 | out = options.fetch(:out) { NullObject.new } 53 | messages = Array(message) 54 | command = "cowsay" 55 | command << " -W #{width}" 56 | command << " -e '#{options[:strings][:eyes]}'" unless eyes.nil? 57 | command << " -f #{options[:cowfile]}" unless cowfile.nil? 58 | 59 | results = messages.map { |message| 60 | checked_popen(command, "w+", lambda{message}) do |process| 61 | process.write(message) 62 | process.close_write 63 | process.read 64 | end 65 | } 66 | output = results.join("\n") 67 | out << output 68 | 69 | @logger.info "Wrote to #{destination}" 70 | output 71 | end 72 | 73 | private 74 | 75 | def checked_popen(command, mode, fail_action) 76 | check_child_exit_status do 77 | @io_class.popen(command, "w+") do |process| 78 | yield(process) 79 | end 80 | end 81 | rescue Errno::EPIPE 82 | fail_action.call 83 | end 84 | 85 | def check_child_exit_status 86 | result = yield 87 | status = $? || OpenStruct.new(:exitstatus => 0) 88 | unless [0,172].include?(status.exitstatus) 89 | raise ArgumentError, 90 | "Command exited with status #{status.exitstatus}" 91 | end 92 | result 93 | end 94 | 95 | def assert(value, message="Assertion failed") 96 | raise Exception, message, caller unless value 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/cowsay_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | require 'tempfile' 3 | 4 | module Cowsay 5 | describe Cow do 6 | 7 | def set_child_exit_status(status) 8 | # $? is read-only so we can't set it manually. Instead we have to start an 9 | # actual process and exit with the given status. 10 | open("|-") do |pipe| exit!(status) if pipe.nil? end 11 | end 12 | 13 | before :each do 14 | set_child_exit_status(0) 15 | @process = stub("process", :read => "OUTPUT").as_null_object 16 | @io_class = stub("IO Class") 17 | @log = stub("Log").as_null_object 18 | @io_class.stub!(:popen).and_yield(@process) 19 | @it = Cow.new(:io_class => @io_class, :logger => @log) 20 | end 21 | 22 | it "should be able to say hello" do 23 | @process.should_receive(:write).with("hello") 24 | @it.say("hello") 25 | end 26 | 27 | it "should start the cowsay process" do 28 | @io_class.should_receive(:popen).with(/^cowsay/, anything) 29 | @it.say("foo") 30 | end 31 | 32 | it "should close the cowsay process after writing" do 33 | @process.should_receive(:write).ordered 34 | @process.should_receive(:close_write).ordered 35 | @it.say("foo") 36 | end 37 | 38 | it "should read process output after closing process input" do 39 | @process.should_receive(:close_write).ordered 40 | @process.should_receive(:read).ordered 41 | @it.say("foo") 42 | end 43 | 44 | it "should return the result of reading from the process" do 45 | @it.say("foo").should be == "OUTPUT" 46 | end 47 | 48 | it "should open cowsay process for read/write" do 49 | @io_class.should_receive(:popen).with(anything, 'w+') 50 | @it.say("foo") 51 | end 52 | 53 | it "should pass the -e flag if 'eyes' string set" do 54 | @io_class.should_receive(:popen).with(/\-e 'oO\'/, anything) 55 | @it.say("moo", :strings => { :eyes => 'oO' }) 56 | end 57 | 58 | context "given an output stream" do 59 | it "should write to given output stream" do 60 | out = StringIO.new 61 | @it.say("moo", :out => out) 62 | out.string.should be == "OUTPUT" 63 | end 64 | 65 | it "should log the filename of output file" do 66 | Tempfile.open('cowsay_spec') do |f| 67 | @log.should_receive(:info).with(/#{f.path}/) 68 | @it.say("moo", :out => f) 69 | end 70 | end 71 | 72 | end 73 | 74 | context "given a non-file output stream" do 75 | it "should log the object in string form" do 76 | out = StringIO.new 77 | out.stub!(:inspect).and_return("") 78 | @log.should_receive(:info).with(//) 79 | @it.say("moo", :out => out) 80 | end 81 | end 82 | 83 | context "when cowsay command is missing" do 84 | it "should just output the bare message" do 85 | @process.should_receive(:write).and_raise(Errno::EPIPE) 86 | @it.say("cluck").should be == "cluck" 87 | end 88 | end 89 | 90 | context "when the command returns a non-zero status" do 91 | it "should raise an error" do 92 | set_child_exit_status(1) 93 | lambda do 94 | @it.say("moo") 95 | end.should raise_error(ArgumentError) 96 | end 97 | end 98 | 99 | context "multiple messages" do 100 | it "should render each message in order" do 101 | @process.should_receive(:write).with("foo").ordered 102 | @process.should_receive(:write).with("bar").ordered 103 | @it.say(["foo", "bar"]) 104 | end 105 | end 106 | 107 | context "nil message" do 108 | it "should return empty string" do 109 | @it.say(nil).should be == "" 110 | end 111 | end 112 | 113 | context "given a cowfile" do 114 | it "should supply a -f argument on the command line" do 115 | @io_class.should_receive(:popen).with(/-f COWFILE/, anything) 116 | @it.say("moo", :cowfile => "COWFILE") 117 | end 118 | end 119 | 120 | context "given a blank cowfile" do 121 | it "should raise an error" do 122 | lambda do 123 | @it.say("moo", :cowfile => " ") 124 | end.should raise_error(Exception) 125 | end 126 | end 127 | 128 | context "with no width specified" do 129 | it "should pass a width arg of 40" do 130 | @io_class.should_receive(:popen).with(/-W 40/, anything) 131 | @it.say("moo") 132 | end 133 | end 134 | 135 | context "given an integer width" do 136 | it "should pass the specified width argument" do 137 | @io_class.should_receive(:popen).with(/-W 29/, anything) 138 | @it.say("moo", :width => 29) 139 | end 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | -fs 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'cowsay' 4 | require 'spec' 5 | require 'spec/autorun' 6 | 7 | Spec::Runner.configure do |config| 8 | 9 | end 10 | --------------------------------------------------------------------------------