├── .gitignore ├── COPYING ├── Gemfile ├── README.md ├── letters.gemspec ├── lib ├── letters.rb └── letters │ ├── assertion_error.rb │ ├── config.rb │ ├── core_ext.rb │ ├── diff.rb │ ├── empty_error.rb │ ├── helpers.rb │ ├── kill.rb │ ├── kill_error.rb │ ├── nil_error.rb │ ├── patch.rb │ ├── patch │ ├── core.rb │ ├── object.rb │ └── rails.rb │ ├── time_formats.rb │ └── version.rb └── spec ├── letters ├── config_spec.rb ├── core_ext_spec.rb ├── helpers_spec.rb ├── patch │ ├── core_spec.rb │ └── rails_spec.rb ├── patch_spec.rb └── time_formats_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.gem 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 David Jacobs 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 included 12 | 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 NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activesupport" 4 | gem "awesome_print" 5 | gem "colorize" 6 | gem "timecop" 7 | gem "xml-simple" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Letters** is a little alphabetical library that makes sophisticated debugging easy & fun. 2 | 3 | For many of us, troubleshooting begins and ends with the `puts` statement. Others recruit the debugger, too. (Maybe you use `puts` statements to look at changes over time but the debugger to focus on a small bit of code.) These tools are good, but they are the lowest level of how we can debug in Ruby. Letters leverages `puts`, the debugger, control transfer, computer beeps, and other side-effects for more well-rounded visibility into code and state. 4 | 5 | ## Installation ## 6 | 7 | If you're using RubyGems, install Letters with: 8 | 9 | gem install letters 10 | 11 | By default, requiring `"letters"` monkey-patches `Object`. It goes without saying that if you're using Letters in an app that has environments, you probably only want to use it in development. 12 | 13 | ## Debugging with letters ## 14 | 15 | With Letters installed, you have a suite of methods available wherever you want them in your code -- at the end of any expression, in the middle of any pipeline. Most of these methods will output some form of information, though there are more sophisticated ones that pass around control of the application. 16 | 17 | There are almost 20 Letters methods so far. You can find them [in the documentation](http://lettersrb.com/api). 18 | 19 | Let's use with the `o` method as an example. It is one of the most familiar methods. Calling it prints the receiver to STDOUT and returns the receiver: 20 | 21 | ```ruby 22 | { foo: "bar" }.o 23 | # => { foo: "bar" } 24 | # prints { foo: "bar" } 25 | ``` 26 | 27 | That's simple enough, but not really useful. Things get interesting when you're in a pipeline: 28 | 29 | ```ruby 30 | words.grep(/interesting/). 31 | map(&:downcase). 32 | group_by(&:length). 33 | values_at(5, 10). 34 | slice(0..2). 35 | join(", ") 36 | ``` 37 | 38 | If I want to know the state of your code after lines 3 and 5, all I have to do is add `.o` to each one: 39 | 40 | ```ruby 41 | words.grep(/interesting/). 42 | map(&:downcase). 43 | group_by(&:length).o. 44 | values_at(5, 10). 45 | slice(0..2).o. 46 | join(", ") 47 | ``` 48 | 49 | Because the `o` method (and nearly every Letters method) returns the original object, introducing it is only ever for side effects -- it won't change the output of your code. 50 | 51 | This is significantly easier than breaking apart the pipeline using variable assignment or a hefty `tap` block. 52 | 53 | The `o` method takes options, too, so you can add a prefix message to the output or choose another output format -- like [YAML]() or [pretty print](). 54 | 55 | ## The methods ## 56 | 57 | Here are the methods, listed with any options that can be passed in to modify their behavior. Some options are available to all methods and are not listed in the table below: 58 | 59 | - `:message (string)`: Print out the specified message as the method is being called. 60 | - `:line_no (boolean)`: Print out the line number where a method is called as it is being called 61 | - `:disable (boolean)`: Disable this method's side effects 62 | 63 | You can easily set these for an entire project using global configuration if you wish (see below). 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 |
LetterCommandOptionsDescription
aAssert:error_classasserts in the context of its receiver or Letters::AssertionError
bBeepcauses your terminal to beep
cCallstackprints the current callstack
dDebuggerpasses control to the debugger
d1/d2Diff:format,:streamprints a diff between first and second receivers
eEmptyraises a Letters::EmptyError if its receiver is empty
fFile:format, :namewrites its receiver into a file in a given format
jJump(&block)executes its block in the context of its receiver
kKill:onraises Letters::KillError at a specified number of calls
lLogger:format, :levellogs its receivers on the available logger instance
mMark as tainted(true|false)taints (or untaints) its receiver
nNilraises a Letters::NilError if its receiver is nil
oOutput:format, :streamprints its receiver to standard output
rRi(method name as symbol)prints RI documentation of its receiver class
sSafety(level number)bumps the safety level (by one or as specified)
tTimestamp:time_formatprints out the current timestamp
185 | 186 | See the [full documentation](http://lettersrb.com/api) for examples and more detailed explanations. 187 | 188 | ## Configuration ## 189 | 190 | For maximum productivity, you can tune and tweak each Letters method to fit your own tastes. Want to name put files somewhere else? No problem. Don't like YAML? Default `f` to use Pretty Print instead! The world of defaults is your oyster. 191 | 192 | ```ruby 193 | Letters.config do 194 | f :format => "pp", :name => "my-special-file" 195 | end 196 | ``` 197 | 198 | You can also change options globally, for methods where the global option is appropriate. For example, if you want every Letters method to print out its line number when called, you can do this for all methods at once: 199 | 200 | ```ruby 201 | Letters.config do 202 | all :line_no => true 203 | end 204 | ``` 205 | 206 | To disable all Letters, for example if you're worried about them getting into a production environment: 207 | 208 | ```ruby 209 | Letters.config do 210 | all :disable => true 211 | end 212 | ``` 213 | -------------------------------------------------------------------------------- /letters.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/letters/version", __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.specification_version = 3 if s.respond_to? :specification_version 5 | s.required_rubygems_version = ">= 1.3.6" 6 | 7 | s.name = "letters" 8 | s.version = Letters::VERSION 9 | 10 | s.platform = Gem::Platform::RUBY 11 | s.homepage = "http://lettersrb.com" 12 | s.author = "David Jacobs" 13 | s.email = "david@wit.io" 14 | s.summary = "A tiny debugging library for Ruby" 15 | s.description = "Letters brings Ruby debugging into the 21st century. It leverages print, the debugger, control transfer, even computer beeps to let you see into your code's state." 16 | 17 | s.files = Dir["lib/**/*"] + [ 18 | "README.md", 19 | "COPYING", 20 | "Gemfile" 21 | ] 22 | 23 | s.test_files = Dir["spec/**/*"] 24 | 25 | s.require_path = "lib" 26 | 27 | s.add_dependency "activesupport" 28 | s.add_dependency "awesome_print" 29 | s.add_dependency "colorize" 30 | s.add_dependency "xml-simple" 31 | 32 | s.add_development_dependency "rspec" 33 | s.add_development_dependency "timecop" 34 | end 35 | -------------------------------------------------------------------------------- /lib/letters.rb: -------------------------------------------------------------------------------- 1 | require "letters/patch/object" 2 | -------------------------------------------------------------------------------- /lib/letters/assertion_error.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | class AssertionError < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/letters/config.rb: -------------------------------------------------------------------------------- 1 | require "letters/assertion_error" 2 | 3 | module Letters 4 | def self.defaults 5 | defaults = { 6 | a: { error_class: Letters::AssertionError }, 7 | d1: { dup: false }, 8 | d2: { format: "ap" }, 9 | f: { format: "yaml", name: "log" }, 10 | k: { on: 0 }, 11 | l: { level: "info", format: "yaml" }, 12 | o: { format: "ap", stream: $stdout }, 13 | t: { time_format: "millis" } 14 | } 15 | 16 | defaults.tap do |hash| 17 | hash.default_proc = lambda {|h, k| h[k] = Hash.new } 18 | end 19 | end 20 | 21 | def self.global_defaults=(opts) 22 | @global_defaults = opts 23 | end 24 | 25 | def self.global_defaults 26 | @global_defaults || {} 27 | end 28 | 29 | def self.user_defaults 30 | @user_defaults ||= Hash.new {|h, k| h[k] = Hash.new } 31 | end 32 | 33 | def self.defaults_with(letter, opts={}) 34 | [global_defaults, defaults[letter], user_defaults[letter], opts].reduce({}, &:merge) 35 | end 36 | 37 | def self.reset_config! 38 | global_defaults.clear 39 | user_defaults.clear 40 | end 41 | 42 | def self.config(&block) 43 | Letters::Config.class_eval(&block) 44 | end 45 | 46 | module Config 47 | define_singleton_method("all") do |opts={}| 48 | Letters.global_defaults = opts 49 | end 50 | 51 | ("a".."z").each do |letter| 52 | define_singleton_method(letter) do |opts={}| 53 | Letters.user_defaults[letter.to_sym] = opts 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/letters/core_ext.rb: -------------------------------------------------------------------------------- 1 | require "letters/config" 2 | require "letters/helpers" 3 | require "letters/diff" 4 | require "letters/kill" 5 | require "letters/time_formats" 6 | require "letters/empty_error" 7 | require "letters/kill_error" 8 | require "letters/nil_error" 9 | 10 | module Letters 11 | module CoreExt 12 | DELIM = "-" * 20 13 | 14 | def ubertap(letter, opts={}, orig_caller=[], &block) 15 | full_opts = Letters.defaults_with(letter, opts) 16 | 17 | unless full_opts[:disable] 18 | Helpers.message full_opts 19 | Helpers.print_line(orig_caller[0]) if full_opts[:line_no] 20 | 21 | tap do |o| 22 | block.call(o, full_opts) 23 | end 24 | end 25 | end 26 | 27 | # Assert 28 | def a(opts={}, &block) 29 | ubertap(:a, opts, caller) do |o, full_opts| 30 | if block_given? && !o.instance_eval(&block) 31 | raise full_opts[:error_class] 32 | end 33 | end 34 | end 35 | 36 | # Beep 37 | def b(opts={}) 38 | ubertap(:b, opts, caller) do |_, _| 39 | $stdout.print "\a" 40 | end 41 | end 42 | 43 | # Callstack 44 | def c(opts={}) 45 | ubertap(:b, opts, caller) do |_, full_opts| 46 | Helpers.out caller(4), full_opts 47 | end 48 | end 49 | 50 | # Debug 51 | def d(opts={}) 52 | ubertap(:d, opts, caller) do |_, _| 53 | Helpers.call_debugger 54 | end 55 | end 56 | 57 | # Diff 1 58 | def d1(opts={}) 59 | ubertap(:d1, opts, caller) do |o, _| 60 | Letters.object_for_diff = o 61 | end 62 | end 63 | 64 | # Diff 2 65 | def d2(opts={}) 66 | ubertap(:d2, opts, caller) do |o, full_opts| 67 | diff = Helpers.diff(Letters.object_for_diff, o) 68 | Helpers.out diff, full_opts 69 | Letters.object_for_diff = nil 70 | end 71 | end 72 | 73 | # Empty check 74 | def e(opts={}) 75 | opts.merge! :error_class => EmptyError 76 | ubertap(:e, opts, caller) do |o, full_opts| 77 | o.a(full_opts) { !empty? } 78 | end 79 | end 80 | 81 | # File 82 | def f(opts={}) 83 | ubertap(:f, opts, caller) do |o, full_opts| 84 | suffixes = [""] + (1..50).to_a 85 | deduper = suffixes.detect {|x| !File.directory? "#{full_opts[:name]}#{x}" } 86 | 87 | File.open("#{full_opts[:name]}#{deduper}", "w+") do |file| 88 | # Override :stream 89 | full_opts.merge! :stream => file 90 | Helpers.out o, full_opts 91 | end 92 | end 93 | end 94 | 95 | # Jump 96 | def j(opts={}, &block) 97 | ubertap(:j, opts, caller) do |o, full_opts| 98 | o.instance_eval &block 99 | end 100 | end 101 | 102 | # Kill 103 | def k(opts={}) 104 | opts.merge! :error_class => KillError 105 | ubertap(:k, opts, caller) do |o, full_opts| 106 | # Support :max option until I can deprecate it 107 | full_opts[:on] ||= full_opts[:max] 108 | 109 | Letters.kill_count ||= 0 110 | 111 | if Letters.kill_count >= full_opts[:on] 112 | Letters.kill_count = 0 113 | o.a(full_opts) { false } 114 | end 115 | 116 | Letters.kill_count += 1 117 | end 118 | end 119 | 120 | # Log 121 | def l(opts={}) 122 | ubertap(:l, opts, caller) do |o, full_opts| 123 | begin 124 | logger.send(full_opts[:level], full_opts[:message]) if full_opts[:message] 125 | logger.send(full_opts[:level], Helpers.send(full_opts[:format], o)) 126 | rescue 127 | $stdout.puts "[warning] No logger available" 128 | end 129 | end 130 | end 131 | 132 | # Taint and untaint object 133 | def m(taint=true, opts={}) 134 | ubertap(:m, opts, caller) do |o, _| 135 | if taint 136 | o.taint 137 | else 138 | o.untaint 139 | end 140 | end 141 | end 142 | 143 | # Nil check 144 | def n(opts={}) 145 | opts.merge! :error_class => NilError 146 | ubertap(:n, opts, caller) do |o, full_opts| 147 | o.a(full_opts) { !nil? } 148 | end 149 | end 150 | 151 | # Print to STDOUT 152 | def o(opts={}, &block) 153 | ubertap(:o, opts, caller) do |o, full_opts| 154 | Helpers.message full_opts 155 | obj = block_given? ? o.instance_eval(&block) : o 156 | Helpers.out obj, full_opts 157 | end 158 | end 159 | 160 | # RI 161 | def r(method=nil, opts={}) 162 | ubertap(:r, opts, caller) do |o, _| 163 | method_or_empty = method ? "##{method}" : method 164 | system "ri #{o.class}#{method_or_empty}" 165 | end 166 | end 167 | 168 | # Change safety level 169 | def s(level=nil, opts={}) 170 | ubertap(:s, opts) do |_, _| 171 | level ||= $SAFE + 1 172 | Helpers.change_safety level 173 | end 174 | end 175 | 176 | # Timestamp 177 | def t(opts={}) 178 | ubertap(:t, opts) do |_, full_opts| 179 | Helpers.out Time.now.to_s(full_opts[:time_format].to_sym), full_opts 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/letters/diff.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | def self.object_for_diff=(object) 3 | @@object = object 4 | end 5 | 6 | def self.object_for_diff 7 | @@object if defined?(@@object) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/letters/empty_error.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | class EmptyError < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/letters/helpers.rb: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | require "pathname" 3 | 4 | module Letters 5 | module Helpers 6 | def self.diff(obj1, obj2) 7 | case obj2 8 | when Hash 9 | { 10 | removed: obj1.reject {|k, v| obj2.include? k }, 11 | added: obj2.reject {|k, v| obj1.include? k }, 12 | updated: obj2.select {|k, v| obj1.include?(k) && obj1[k] != v } 13 | } 14 | when String 15 | diff(obj1.split("\n"), obj2.split("\n")) 16 | else 17 | { 18 | removed: Array(obj1 - obj2), 19 | added: Array(obj2 - obj1) 20 | } 21 | end 22 | rescue 23 | raise "cannot diff the two marked objects" 24 | end 25 | 26 | def self.message(opts={}) 27 | out(opts[:message], opts) if opts[:message] 28 | end 29 | 30 | def self.out(object, opts={}) 31 | opts = { stream: $stdout, format: "string" }.merge opts 32 | opts[:stream].puts Helpers.send(opts[:format], object) 33 | end 34 | 35 | def self.ap(object) 36 | require "awesome_print" 37 | object.awesome_inspect 38 | end 39 | 40 | def self.json(object) 41 | require "json" 42 | object.to_json 43 | end 44 | 45 | def self.pp(object) 46 | require "pp" 47 | object.pretty_inspect 48 | end 49 | 50 | def self.string(object) 51 | object.to_s 52 | end 53 | 54 | def self.xml(object) 55 | require "xmlsimple" 56 | XmlSimple.xml_out(object, { "KeepRoot" => true }) 57 | end 58 | 59 | def self.yaml(object) 60 | require "yaml" 61 | object.to_yaml 62 | end 63 | 64 | def self.print_line(caller_line) 65 | file, line_no = caller_line.split.first.sub(/:in$/, "").split(":") 66 | 67 | line = if File.exist?(file) && File.file?(file) 68 | File.readlines(file)[Integer(line_no) - 1] 69 | else 70 | "" 71 | end 72 | 73 | rel_file = Pathname.new(file).expand_path.relative_path_from(Pathname.getwd) 74 | 75 | puts "Letters call at #{file}, line #{line_no}".underline 76 | puts 77 | puts " " + line.strip.chomp.sub(/^\W*/, "").green 78 | puts 79 | end 80 | 81 | def self.pretty_callstack(callstack) 82 | home = ENV["MY_RUBY_HOME"] 83 | 84 | parsed = callstack.map do |entry| 85 | line, line_no, method_name = entry.split ":" 86 | 87 | { 88 | line: line.gsub(home + "/", "").green, 89 | line_no: line_no.yellow, 90 | method_name: method_name.scan(/`([^\']+)'/).first.first.light_blue 91 | } 92 | end 93 | 94 | headers = { 95 | line: "Line".green, 96 | line_no: "No.".yellow, 97 | method_name: "Method".light_blue 98 | } 99 | 100 | parsed.unshift headers 101 | 102 | longest_line = 103 | parsed.map {|entry| entry[:line] }. 104 | sort_by(&:length). 105 | last 106 | 107 | longest_method = 108 | parsed.map {|entry| entry[:method_name] }. 109 | sort_by(&:length). 110 | last 111 | 112 | formatter = "%#{longest_method.length}{method_name} %-#{longest_line.length}{line} %{line_no}\n" 113 | 114 | parsed.map {|h| formatter % h }.join 115 | end 116 | 117 | # This provides a mockable method for testing 118 | def self.call_debugger 119 | if (defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx') 120 | require 'rubinius/debugger' 121 | Rubinius::Debugger.start 122 | else 123 | require 'debug' 124 | end 125 | 126 | nil 127 | end 128 | 129 | def self.change_safety(safety) 130 | $SAFE = safety 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/letters/kill.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | def self.kill_count=(count) 3 | @@kill_count = count 4 | end 5 | 6 | def self.kill_count 7 | @@kill_count if defined?(@@kill_count) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/letters/kill_error.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | class KillError < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/letters/nil_error.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | class NilError < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/letters/patch.rb: -------------------------------------------------------------------------------- 1 | require "letters/core_ext" 2 | 3 | module Letters 4 | def self.patch!(obj) 5 | case obj 6 | when Class 7 | obj.instance_eval do 8 | include Letters::CoreExt 9 | end 10 | when Object 11 | obj.extend Letters::CoreExt 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/letters/patch/core.rb: -------------------------------------------------------------------------------- 1 | require "letters/patch" 2 | require "set" 3 | 4 | Letters.patch! Numeric 5 | Letters.patch! Symbol 6 | Letters.patch! String 7 | Letters.patch! Regexp 8 | Letters.patch! Array 9 | Letters.patch! Set 10 | Letters.patch! Hash 11 | Letters.patch! Range 12 | Letters.patch! NilClass 13 | Letters.patch! TrueClass 14 | Letters.patch! FalseClass 15 | -------------------------------------------------------------------------------- /lib/letters/patch/object.rb: -------------------------------------------------------------------------------- 1 | require "letters/patch" 2 | 3 | Letters.patch! Object 4 | -------------------------------------------------------------------------------- /lib/letters/patch/rails.rb: -------------------------------------------------------------------------------- 1 | require "letters/patch/core" 2 | 3 | begin 4 | Letters.patch! ActiveRecord::Base 5 | Letters.patch! ActionController::Base 6 | Letters.patch! ActionMailer::Base 7 | rescue 8 | $stderr.puts "[warning] tried to patch Rails without Rails being loaded" 9 | end 10 | -------------------------------------------------------------------------------- /lib/letters/time_formats.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/date_time/conversions" 2 | 3 | module Letters 4 | Time::DATE_FORMATS[:millis] = "%m/%d/%Y %H:%M:%S.%L" 5 | end 6 | -------------------------------------------------------------------------------- /lib/letters/version.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | VERSION = "0.4.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/letters/config_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Letters 4 | describe "configuration" do 5 | let(:hash) { { a: "b" } } 6 | 7 | before do 8 | Letters.config do 9 | f :format => "pp" 10 | end 11 | File.exist?("log").should be_false 12 | end 13 | 14 | after do 15 | Letters.reset_config! 16 | FileUtils.rm_rf "log" 17 | end 18 | 19 | describe ".config" do 20 | it "allows default argument configuration" do 21 | hash.f 22 | File.read("log").should == hash.pretty_inspect 23 | end 24 | 25 | it "allows global default argument configuration" do 26 | Letters.config do 27 | all :line_no => true 28 | end 29 | 30 | $stdout.should_receive(:puts).exactly(4).times 31 | hash.b 32 | end 33 | 34 | it "allows specific defaults to override global defaults" do 35 | Letters.config do 36 | all :line_no => true 37 | b :line_no => false 38 | end 39 | 40 | $stdout.should_receive(:puts).never 41 | hash.b 42 | end 43 | 44 | it "allows disabling of all letters" do 45 | Letters.config do 46 | all :disable => true 47 | b :line_no => true 48 | end 49 | 50 | $stdout.should_receive(:puts).never 51 | hash.b 52 | end 53 | end 54 | 55 | describe ".reset_config!" do 56 | it "clears out the config hash" do 57 | Letters.reset_config! 58 | hash.f 59 | File.read("log").should == hash.to_yaml 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/letters/core_ext_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Letters 4 | describe CoreExt do 5 | let(:hash) { Hash.new } 6 | 7 | before do 8 | @old_dir = Dir.getwd 9 | FileUtils.mkdir_p "tmp" 10 | Dir.chdir "tmp" 11 | end 12 | 13 | after do 14 | Dir.chdir @old_dir 15 | FileUtils.rm_rf "tmp" 16 | end 17 | 18 | it "all letter methods but #e, #k and #n return the original object" do 19 | # Prevent output and debugging 20 | Helpers.should_receive(:call_debugger).any_number_of_times 21 | $stdout.should_receive(:puts).any_number_of_times 22 | hash.should_receive(:system).any_number_of_times 23 | Helpers.should_receive(:change_safety).any_number_of_times 24 | 25 | ("a".."z").to_a.reject do |letter| 26 | letter =~ /[ekjn]/ 27 | end.select do |letter| 28 | hash.respond_to? letter 29 | end.each do |letter| 30 | hash.send(letter).should == hash 31 | end 32 | 33 | # Methods that can take a block 34 | hash.j { nil }.should == hash 35 | hash.o { nil }.should == hash 36 | end 37 | 38 | it "all letter methods have the option of outputting the line where they are called" do 39 | pending "I need to figure out a good way to test this" 40 | # hash.j(:line_no => true) { nil }.should == hash 41 | end 42 | 43 | describe "#a (assert)" do 44 | it "jumps into the receiver's calling context" do 45 | lambda do 46 | [1, 2, 3].a { count } 47 | end.should_not raise_error 48 | end 49 | 50 | it "raises a Letters::AssertionError if the block returns false" do 51 | lambda do 52 | [1, 2, 3].a { count > 3 } 53 | end.should raise_error(Letters::AssertionError) 54 | end 55 | 56 | it "raises a Letters::AssertionError if the block returns nil" do 57 | lambda do 58 | [1, 2, 3].a { nil } 59 | end.should raise_error(Letters::AssertionError) 60 | end 61 | 62 | it "does nothing if the block returns a truthy value" do 63 | [1, 2, 3].a { count < 4 }.should == [1, 2, 3] 64 | end 65 | end 66 | 67 | describe "#c (callstack)" do 68 | it "outputs the current call trace then returns the object" do 69 | $stdout.should_receive(:puts).with kind_of String 70 | hash.c 71 | end 72 | end 73 | 74 | describe "#d (debug)" do 75 | it "enters the debugger and then returns the object" do 76 | Helpers.should_receive(:call_debugger) 77 | hash.d 78 | end 79 | end 80 | 81 | describe "#d1, #d2 (smart object diff)" do 82 | it "outputs the difference between two arrays" do 83 | arr1, arr2 = [1, 2, 3], [3, 4, 5] 84 | expected_diff = Helpers.diff(arr1, arr2) 85 | $stdout.should_receive(:puts).with(expected_diff.awesome_inspect).once 86 | 87 | arr1.d1.should == arr1 88 | arr2.d2.should == arr2 89 | end 90 | end 91 | 92 | describe "#e (empty check)" do 93 | it "raises an error if the receiver is empty" do 94 | lambda { "".e }.should raise_error(EmptyError) 95 | end 96 | 97 | it "does nothing if the receiver is not empty" do 98 | lambda { "string".e }.should_not raise_error 99 | "string".n.should == "string" 100 | end 101 | end 102 | 103 | describe "#f (file)" do 104 | describe "when no filename or output format are given" do 105 | it "writes the object as YAML to a file named 'log'" do 106 | File.exist?("log").should_not be_true 107 | hash.f 108 | File.exist?("log").should be_true 109 | File.read("log").should == hash.to_yaml 110 | end 111 | end 112 | 113 | describe "when a file name, but no output format is given" do 114 | it "writes the object as YAML to the named file" do 115 | hash.f(:name => "object") 116 | File.exist?("object").should be_true 117 | File.read("object").should == hash.to_yaml 118 | end 119 | end 120 | 121 | describe "when an output format, but no file name is given" do 122 | it "writes the object as that format to a file named 'log'" do 123 | hash.f(:format => :ap) 124 | File.exist?("log").should be_true 125 | File.read("log").chomp.should == hash.awesome_inspect 126 | end 127 | end 128 | 129 | describe "when 'log' is a directory" do 130 | before { FileUtils.mkdir "log" } 131 | after { FileUtils.rm_rf "log" } 132 | 133 | it "appends the first integer that de-dupes the name" do 134 | File.exist?("log1").should be_false 135 | lambda { hash.f }.should_not raise_error 136 | File.exist?("log1").should be_true 137 | end 138 | end 139 | end 140 | 141 | describe "#j (jump)" do 142 | it "jumps into the object's context" do 143 | a = nil 144 | hash.j { a = count } 145 | a.should == 0 146 | end 147 | 148 | it "allows for IO, even in object context" do 149 | $stdout.should_receive(:puts).with(0) 150 | hash.j { puts count } 151 | end 152 | end 153 | 154 | describe "#k (kill)" do 155 | it "raises a KillError immediately by default" do 156 | lambda { hash.k }.should raise_error(KillError) 157 | end 158 | 159 | it "does not raises if number of calls are below the specified number" do 160 | lambda { hash.k(:on => 1) }.should_not raise_error 161 | end 162 | 163 | it "raises a KillError if number of calls has reached the specified number" do 164 | count = 0 165 | hash.k(:on => 2) 166 | lambda do 167 | hash.k(:on => 2) 168 | end.should raise_error(KillError) 169 | end 170 | end 171 | 172 | describe "#l (log)" do 173 | it "logs the object if a logger is present and then returns the object" do 174 | logger = double "logger" 175 | logger.should_receive(:info).with(hash.to_yaml) 176 | hash.should_receive(:logger).and_return(logger) 177 | hash.l 178 | end 179 | 180 | it "prints an warning if a logger is not present and then returns the object" do 181 | $stdout.should_receive(:puts).with("[warning] No logger available") 182 | hash.l 183 | end 184 | 185 | it "logs the object if a logger is present and then returns the object" do 186 | logger = double 'logger' 187 | logger.should_receive(:info).never 188 | logger.should_receive(:error).with(hash.to_yaml) 189 | hash.should_receive(:logger).and_return(logger) 190 | hash.l(:level => "error") 191 | end 192 | end 193 | 194 | describe "#m (mark object as tainted/untainted)" do 195 | it "with no argument or `true`, marks the receiver as tainted" do 196 | lambda do 197 | hash.m 198 | end.should change { hash.tainted? }.from(false).to(true) 199 | end 200 | 201 | it "when passed `false`, marks the receiver as untainted" do 202 | hash.taint 203 | lambda do 204 | hash.m(false) 205 | end.should change { hash.tainted? }.from(true).to(false) 206 | end 207 | end 208 | 209 | describe "#n (nil check)" do 210 | it "raises an error if the receiver is nil" do 211 | lambda { nil.n }.should raise_error(NilError) 212 | end 213 | 214 | it "does nothing if the receiver is not nil" do 215 | lambda { hash.n }.should_not raise_error 216 | end 217 | end 218 | 219 | describe "#o (print)" do 220 | describe "when no format is given" do 221 | it "writes the object as awesome_print to STDOUT" do 222 | $stdout.should_receive(:puts).with(hash.awesome_inspect) 223 | hash.o 224 | end 225 | end 226 | 227 | describe "when a format is given" do 228 | it "writes the object as that format to STDOUT" do 229 | $stdout.should_receive(:puts).with(hash.to_yaml) 230 | hash.o(:format => :yaml) 231 | end 232 | end 233 | 234 | describe "when a block is given" do 235 | it "write the result of the block, executed in the object's context" do 236 | $stdout.should_receive(:puts).with(hash.length.awesome_inspect) 237 | hash.o { length }.should == hash 238 | end 239 | end 240 | end 241 | 242 | describe "#r (ri)" do 243 | it "displays RI information for the receiver's class, if available" do 244 | hash.should_receive(:system).with("ri Hash") 245 | hash.r 246 | end 247 | 248 | it "can narrow its scope to a single method" do 249 | hash.should_receive(:system).with("ri Hash#new") 250 | hash.r(:new) 251 | end 252 | end 253 | 254 | describe "#s (safety)" do 255 | it "without argument, bumps the safety level by one" do 256 | Helpers.should_receive(:change_safety).with(1) 257 | hash.s 258 | end 259 | 260 | it "bumps the safety level to the specified level if possible" do 261 | Helpers.should_receive(:change_safety).with(4) 262 | hash.s(4) 263 | end 264 | 265 | it "throws an exception if the specified level is invalid" do 266 | # Simulate changing safety level from higher level 267 | Helpers.should_receive(:change_safety).with(3).and_raise 268 | lambda do 269 | hash.s(3) 270 | end.should raise_error 271 | end 272 | end 273 | 274 | describe "#t (timestamp)" do 275 | it "without :stream, prints the current time to STDOUT" do 276 | time = Time.now 277 | Timecop.freeze(time) do 278 | $stdout.should_receive(:puts).with(time.to_s(:millis)).twice 279 | {}.t.select {|k,v| k =~ /foo/ }.t 280 | end 281 | end 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /spec/letters/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Letters 4 | describe Helpers do 5 | let(:hash) { Hash.new } 6 | 7 | describe ".diff" do 8 | it "returns the difference between two arrays" do 9 | array1 = [1, 2, 3] 10 | array2 = [3, 4, 5] 11 | Helpers.diff(array1, array2).should == { 12 | removed: [1, 2], 13 | added: [4, 5] 14 | } 15 | end 16 | 17 | it "returns the difference between two hashes" do 18 | hash1 = { a: "foo", b: "bar" } 19 | hash2 = { b: "baz", c: "bat" } 20 | 21 | Helpers.diff(hash1, hash2).should == { 22 | removed: { a: "foo" }, 23 | added: { c: "bat" }, 24 | updated: { b: "baz" } 25 | } 26 | end 27 | 28 | it "returns the difference between two sets" do 29 | set1 = Set.new([1, 2, 3]) 30 | set2 = Set.new([3, 4, 5]) 31 | Helpers.diff(set1, set2).should == { 32 | removed: [1, 2], 33 | added: [4, 5] 34 | } 35 | end 36 | 37 | it "returns the difference between two strings" do 38 | string1 = "Line 1\nLine 2" 39 | string2 = "Line 1 modified\nLine 2\nLine 3" 40 | Helpers.diff(string1, string2).should == { 41 | removed: ["Line 1"], 42 | added: ["Line 1 modified", "Line 3"] 43 | } 44 | end 45 | 46 | it "returns the difference between two objects of the same hierarchy" do 47 | DiffableClass = Class.new do 48 | attr_accessor :vals 49 | 50 | def initialize(vals) 51 | self.vals = vals 52 | end 53 | 54 | def -(other) 55 | vals - other.vals 56 | end 57 | end 58 | 59 | dc1 = DiffableClass.new([1, 2, 3]) 60 | dc2 = DiffableClass.new([3, 4, 5]) 61 | Helpers.diff(dc1, dc2).should == { 62 | removed: [1, 2], 63 | added: [4, 5] 64 | } 65 | end 66 | 67 | it "throws an exception if the objects are not of the same type" do 68 | lambda do 69 | Helpers.diff(Object.new, Hash.new) 70 | end.should raise_error 71 | end 72 | end 73 | 74 | describe ".awesome_print" do 75 | it "outputs the YAML representation of the object then returns the object" do 76 | Helpers.ap(hash).should == "{}" 77 | end 78 | end 79 | 80 | describe ".pretty_print" do 81 | it "outputs the pretty-print representation of the object and then returns the object" do 82 | Helpers.pp(hash).should == "{}\n" 83 | end 84 | end 85 | 86 | describe ".string" do 87 | it "outputs the representation of the object returned by #to_s" do 88 | complex_hash = { foo: "bar", baz: [1, 2, 3] } 89 | Helpers.string(complex_hash).should == complex_hash.to_s 90 | end 91 | end 92 | 93 | describe ".xml" do 94 | it "outputs the XML representation of the object and then returns the object" do 95 | Helpers.xml(hash).should == "\n" 96 | end 97 | end 98 | 99 | describe ".yaml" do 100 | it "outputs the YAML representation of the object and then returns the object" do 101 | Helpers.yaml(hash).strip.should == "--- {}" 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/letters/patch/core_spec.rb: -------------------------------------------------------------------------------- 1 | module Letters 2 | describe "core patches" do 3 | it "adds methods to each of the specified core classes" do 4 | require "letters/patch/core" 5 | 6 | instances = [1, "string", :symbol, 7 | /regexp/, [], Set.new, 8 | {}, 1..3, nil, 9 | true, false] 10 | 11 | instances.each do |instance| 12 | instance.should respond_to(:a) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/letters/patch/rails_spec.rb: -------------------------------------------------------------------------------- 1 | class ActiveRecord 2 | class Base 3 | def -(other) 4 | self 5 | end 6 | end 7 | end 8 | 9 | class ActionController 10 | class Base; end 11 | end 12 | 13 | class ActionMailer 14 | class Base; end 15 | end 16 | 17 | describe "Rails patches" do 18 | before do 19 | require "letters/patch/rails" 20 | end 21 | 22 | it "adds methods to each of the specified core classes" do 23 | [ActiveRecord::Base, 24 | ActionController::Base, 25 | ActionMailer::Base].each do |klass| 26 | klass.new.should respond_to(:a) 27 | end 28 | end 29 | 30 | it "allows d1/d2 pairs" do 31 | $stdout.should_receive(:puts) 32 | 33 | lambda do 34 | ActiveRecord::Base.new.d1 35 | ActiveRecord::Base.new.d2 36 | end.should_not raise_error 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/letters/patch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Letters 4 | describe ".patch!" do 5 | before do 6 | # Hide output 7 | $stdout.should_receive(:puts) 8 | end 9 | 10 | it "adds Letters::CoreExt to classes" do 11 | Klass = Class.new 12 | Letters.patch! Klass 13 | k = Klass.new 14 | k.o.should == k 15 | end 16 | 17 | it "adds Letters::CoreExt to objects" do 18 | obj = Object.new 19 | Letters.patch! obj 20 | obj.o.should == obj 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/letters/time_formats_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Letters 4 | describe "time formats" do 5 | it "allows for easy manipulation of timestamp displays" do 6 | Time.utc(2012, "jan", 1, 13, 15, 1).tap do |time| 7 | time.to_s(:millis).should == "01/01/2012 13:15:01.000" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "letters" 2 | require "awesome_print" 3 | require "timecop" 4 | require "fileutils" 5 | --------------------------------------------------------------------------------