├── .tldr.yml ├── lib ├── todo_or_die │ ├── version.rb │ └── overdue_error.rb └── todo_or_die.rb ├── .gitignore ├── Rakefile ├── bin ├── setup └── console ├── Gemfile ├── .claude └── settings.json ├── .github └── workflows │ └── main.yml ├── todo_or_die.gemspec ├── test ├── helper.rb └── todo_or_die_test.rb ├── LICENSE.txt ├── CHANGELOG.md ├── Gemfile.lock └── README.md /.tldr.yml: -------------------------------------------------------------------------------- 1 | parallel: false 2 | -------------------------------------------------------------------------------- /lib/todo_or_die/version.rb: -------------------------------------------------------------------------------- 1 | module TodoOrDie 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/todo_or_die/overdue_error.rb: -------------------------------------------------------------------------------- 1 | module TodoOrDie 2 | class OverdueTodo < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "standard/rake" 3 | require "tldr/rake" 4 | 5 | task default: [:tldr, "standard:fix"] 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in todo_or_die.gemspec 6 | gemspec 7 | 8 | gem "rake" 9 | gem "standard" 10 | gem "timecop" 11 | gem "tldr" 12 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "PostToolUse": [ 4 | { 5 | "matcher": "Edit|MultiEdit|Write", 6 | "hooks": [ 7 | { 8 | "type": "command", 9 | "command": "bundle exec tldr --timeout 0.1 --exit-0-on-timeout --exit-2-on-failure" 10 | } 11 | ] 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "todo_or_die" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.2.1' 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Run the default task 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /todo_or_die.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "todo_or_die" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "todo_or_die" 7 | spec.version = TodoOrDie::VERSION 8 | spec.authors = ["Justin Searls"] 9 | spec.email = ["searls@gmail.com"] 10 | 11 | spec.summary = "Write TO​DOs in code that ensure you actually do them" 12 | spec.homepage = "https://github.com/searls/todo_or_die" 13 | 14 | spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do 15 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 16 | end 17 | spec.require_paths = ["lib"] 18 | end 19 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | require "timecop" 3 | Timecop.thread_safe = true 4 | 5 | require "todo_or_die" 6 | 7 | require "tldr" 8 | if defined?(Minitest::Test) 9 | TLDR::MinitestTestBackup = Minitest::Test 10 | Minitest.send(:remove_const, "Test") 11 | end 12 | module Minitest 13 | class Test < TLDR 14 | include TLDR::MinitestCompatibility 15 | end 16 | end 17 | 18 | class UnitTest < Minitest::Test 19 | def teardown 20 | Timecop.return 21 | TodoOrDie.reset 22 | Object.send(:remove_const, :Rails) if defined?(Rails) 23 | end 24 | 25 | def make_it_be_rails(is_production) 26 | rails = Object.const_set(:Rails, Module.new) 27 | 28 | rails.define_singleton_method(:env) do 29 | OpenStruct.new(production?: is_production) 30 | end 31 | 32 | fake_logger = FauxLogger.new 33 | rails.define_singleton_method(:logger) do 34 | fake_logger 35 | end 36 | 37 | fake_logger 38 | end 39 | 40 | class FauxLogger 41 | attr_reader :warning 42 | 43 | def warn(message) 44 | @warning = message 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Justin Searls 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.1.1] - 2022-07-01 8 | 9 | - Fix `warn_by` [#16](https://github.com/searls/todo_or_die/pull/16) 10 | 11 | ## [0.1.0] - 2022-06-27 12 | 13 | - Add `warn_by` option [#14](https://github.com/searls/todo_or_die/pull/14) 14 | 15 | ## [0.0.3] - 2019-11-26 16 | ### Added 17 | - Boolean-returning conditionals or callables (Proc, Block, Lambda) can be passed to the 18 | new `if` argument. `if` can be used instead of or in conjunction with the `by` argument 19 | to die on a specific date OR when a specific condition becomes true. 20 | 21 | ### Changed 22 | - Date strings can now be parsed parsed internally without calling `Time` or `Date` 23 | classes explicitely. 24 | 25 | ## [0.0.2] - 2019-02-15 26 | ### Changed 27 | - Exclude this gem's backtrace location from exceptions thrown to make it easier to find 28 | TODOs. 29 | 30 | ## [0.0.1] - 2019-01-01 31 | 32 | [Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.3...HEAD 33 | [0.0.3]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.2...v0.0.3 34 | [0.0.2]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.1...v0.0.2 35 | [0.0.1]: https://github.com/olivierlacan/keep-a-changelog/releases/tag/v0.0.1 36 | -------------------------------------------------------------------------------- /lib/todo_or_die.rb: -------------------------------------------------------------------------------- 1 | require "time" 2 | require "todo_or_die/version" 3 | require "todo_or_die/overdue_error" 4 | 5 | # The namespace 6 | module TodoOrDie 7 | DEFAULT_CONFIG = { 8 | die: ->(message, due_at, condition) { 9 | error_message = [ 10 | "TODO: \"#{message}\"", 11 | (" came due on #{due_at.strftime("%Y-%m-%d")}" if due_at), 12 | (" and" if due_at && condition), 13 | (" has met the conditions to be acted upon" if condition), 14 | ". Do it!" 15 | ].compact.join("") 16 | 17 | if defined?(Rails) && Rails.env.production? 18 | Rails.logger.warn(error_message) 19 | else 20 | raise TodoOrDie::OverdueTodo, error_message, TodoOrDie.__clean_backtrace(caller) 21 | end 22 | }, 23 | 24 | warn: lambda { |message, due_at, warn_at, condition| 25 | error_message = [ 26 | "TODO: \"#{message}\"", 27 | (" is due on #{due_at.strftime("%Y-%m-%d")}" if due_at), 28 | (" and" if warn_at && condition), 29 | (" has met the conditions to be acted upon" if condition), 30 | ". Don't forget!" 31 | ].compact.join("") 32 | 33 | puts error_message 34 | 35 | Rails.logger.warn(error_message) if defined?(Rails) 36 | } 37 | }.freeze 38 | 39 | def self.config(options = {}) 40 | @config ||= reset 41 | @config.merge!(options) 42 | end 43 | 44 | def self.reset 45 | @config = DEFAULT_CONFIG.dup 46 | end 47 | 48 | FILE_PATH_REGEX = Regexp.new(Regexp.quote(__dir__)).freeze 49 | def self.__clean_backtrace(stack) 50 | stack.delete_if { |line| line =~ FILE_PATH_REGEX } 51 | end 52 | end 53 | 54 | # The main event 55 | def TodoOrDie(message, by: by_omitted = true, if: if_omitted = true, warn_by: warn_by_omitted = true) 56 | due_at = Time.parse(by.to_s) unless by_omitted 57 | warn_at = Time.parse(warn_by.to_s) unless warn_by_omitted 58 | condition = binding.local_variable_get(:if) unless if_omitted 59 | 60 | is_past_due_date = by_omitted || Time.now > due_at 61 | die_condition_met = if_omitted || (condition.respond_to?(:call) ? condition.call : condition) 62 | no_conditions_given = by_omitted && if_omitted && warn_by_omitted 63 | only_warn_condition_given = if_omitted && by_omitted && !warn_by_omitted 64 | 65 | ready_to_die = is_past_due_date && die_condition_met && !only_warn_condition_given 66 | should_die = no_conditions_given || ready_to_die 67 | should_warn = !warn_by_omitted && Time.now > warn_at 68 | 69 | if should_die 70 | die = TodoOrDie.config[:die] 71 | die.call(*[message, due_at, condition].take(die.arity.abs)) 72 | elsif should_warn 73 | warn = TodoOrDie.config[:warn] 74 | warn.call(*[message, due_at, warn_at, condition].take(warn.arity.abs)) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | todo_or_die (0.1.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.3) 10 | attr_extras (7.1.0) 11 | concurrent-ruby (1.3.5) 12 | date (3.4.1) 13 | diff-lcs (1.6.2) 14 | erb (5.0.2) 15 | io-console (0.8.1) 16 | irb (1.15.2) 17 | pp (>= 0.6.0) 18 | rdoc (>= 4.0.0) 19 | reline (>= 0.4.2) 20 | json (2.12.2) 21 | language_server-protocol (3.17.0.5) 22 | lint_roller (1.1.0) 23 | optimist (3.2.1) 24 | parallel (1.27.0) 25 | parser (3.3.8.0) 26 | ast (~> 2.4.1) 27 | racc 28 | patience_diff (1.2.0) 29 | optimist (~> 3.0) 30 | pp (0.6.2) 31 | prettyprint 32 | prettyprint (0.2.0) 33 | prism (1.4.0) 34 | psych (5.2.6) 35 | date 36 | stringio 37 | racc (1.8.1) 38 | rainbow (3.1.1) 39 | rake (13.3.0) 40 | rdoc (6.14.2) 41 | erb 42 | psych (>= 4.0.0) 43 | regexp_parser (2.10.0) 44 | reline (0.6.1) 45 | io-console (~> 0.5) 46 | rubocop (1.75.8) 47 | json (~> 2.3) 48 | language_server-protocol (~> 3.17.0.2) 49 | lint_roller (~> 1.1.0) 50 | parallel (~> 1.10) 51 | parser (>= 3.3.0.2) 52 | rainbow (>= 2.2.2, < 4.0) 53 | regexp_parser (>= 2.9.3, < 3.0) 54 | rubocop-ast (>= 1.44.0, < 2.0) 55 | ruby-progressbar (~> 1.7) 56 | unicode-display_width (>= 2.4.0, < 4.0) 57 | rubocop-ast (1.46.0) 58 | parser (>= 3.3.7.2) 59 | prism (~> 1.4) 60 | rubocop-performance (1.25.0) 61 | lint_roller (~> 1.1) 62 | rubocop (>= 1.75.0, < 2.0) 63 | rubocop-ast (>= 1.38.0, < 2.0) 64 | ruby-progressbar (1.13.0) 65 | standard (1.50.0) 66 | language_server-protocol (~> 3.17.0.2) 67 | lint_roller (~> 1.0) 68 | rubocop (~> 1.75.5) 69 | standard-custom (~> 1.0.0) 70 | standard-performance (~> 1.8) 71 | standard-custom (1.0.2) 72 | lint_roller (~> 1.0) 73 | rubocop (~> 1.50) 74 | standard-performance (1.8.0) 75 | lint_roller (~> 1.1) 76 | rubocop-performance (~> 1.25.0) 77 | stringio (3.1.7) 78 | super_diff (0.16.0) 79 | attr_extras (>= 6.2.4) 80 | diff-lcs 81 | patience_diff 82 | timecop (0.9.10) 83 | tldr (1.1.0) 84 | concurrent-ruby (~> 1.2) 85 | irb (~> 1.10) 86 | super_diff (~> 0.10) 87 | unicode-display_width (3.1.4) 88 | unicode-emoji (~> 4.0, >= 4.0.4) 89 | unicode-emoji (4.0.4) 90 | 91 | PLATFORMS 92 | ruby 93 | x86_64-linux 94 | 95 | DEPENDENCIES 96 | rake 97 | standard 98 | timecop 99 | tldr 100 | todo_or_die! 101 | 102 | BUNDLED WITH 103 | 2.4.19 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TODO or Die! 2 | 3 | TODO or Die NES cart 4 | 5 | ## Usage 6 | 7 | Stick this in your Gemfile and bundle it: 8 | 9 | ```ruby 10 | gem "todo_or_die" 11 | ``` 12 | 13 | Once required, you can mark TODOs for yourself anywhere you like: 14 | 15 | ```ruby 16 | TodoOrDie("Update after APIv2 goes live", by: Date.civil(2019, 2, 4)) 17 | ``` 18 | 19 | To understand why you would ever call a method to write a comment, read on. 20 | 21 | ### The awful way you used to procrastinate 22 | 23 | In the Bad Old Days™, if you had a bit of code you knew you needed to change 24 | later, you might leave yourself a code comment to remind yourself to change it. 25 | For example, here's the real world code comment that inspired this gem: 26 | 27 | ``` ruby 28 | class UsersController < ApiController 29 | # TODO: remember to delete after JS app has propagated 30 | def show 31 | redirect_to root_path 32 | end 33 | end 34 | ``` 35 | 36 | This was bad. The comment did nothing to remind myself or anyone else to 37 | actually delete the code. Because no one was working on this part of the system 38 | for a while, the _continued existence of the redirect_ eventually resulted in an 39 | actual support incident (long story). 40 | 41 | ### The cool new way you put off coding now 42 | 43 | So I did what any programmer would do in the face of an intractable social 44 | problem: I wrote code in the vain hope of solving things without needing to talk 45 | to anyone. And now this gem exists. 46 | 47 | To use it, try replacing one of your TODO comments with something like this: 48 | 49 | ``` ruby 50 | class UsersController < ApiController 51 | TodoOrDie("delete after JS app has propagated", by: "2019-02-04") 52 | def show 53 | redirect_to root_path 54 | end 55 | end 56 | ``` 57 | 58 | Nothing will happen at all until February 4th, at which point the gem will 59 | raise an error whenever this class is loaded until someone deals with it. 60 | 61 | You may also pass a condition, either as a callable (e.g. proc/lambda/method) or 62 | a boolean test: 63 | 64 | ``` ruby 65 | class User < ApplicationRecord 66 | TodoOrDie("delete after someone wins", if: User.count > 1000000) 67 | def is_one_millionth_user? 68 | id == 1000000 69 | end 70 | end 71 | ``` 72 | 73 | You can also pass both `by` and `if` (where both must be met for an error to be 74 | raised) or, I guess, neither (where an error will be raised as soon as 75 | `TodoOrDie` is invoked). 76 | 77 | ### What kind of error? 78 | 79 | It depends on whether you're using [Rails](https://rubyonrails.org) or not. 80 | 81 | #### When you're writing Real Ruby 82 | 83 | If you're not using Rails (i.e. `defined?(Rails)` is false), then the gem will 84 | raise a `TodoOrDie::OverdueError` whenever a TODO is overdue. The message looks 85 | like this: 86 | 87 | ``` 88 | TODO: "Visit Wisconsin" came due on 2016-11-09. Do it! 89 | ``` 90 | 91 | #### When `Rails` is a thing 92 | 93 | If TodoOrDie sees that `Rails` is defined, it'll assume you probably don't want 94 | this tool to run outside development and test, so it'll log the error message to 95 | `Rails.logger.warn` in production (while still raising the error in development 96 | and test). 97 | 98 | ### Wait, won't sprinkling time bombs throughout my app ruin my weekend? 99 | 100 | Sure will! It's "TODO or Die", not "TODO and Remember to Pace Yourself". 101 | 102 | Still, someone will probably get mad if you break production because you forgot 103 | to follow through on removing an A/B test, so I'd [strongly recommend you read 104 | what the default hook actually does](lib/todo_or_die.rb#L8-L16) before this gem 105 | leads to you losing your job. (Speaking of, please note the lack of any warranty 106 | in `todo_or_die`'s [license](LICENSE.txt).) 107 | 108 | To appease your boss, you may customize the gem's behavior by passing in your 109 | own `call`'able lambda/proc/dingus like this: 110 | 111 | ```ruby 112 | TodoOrDie.config( 113 | die: ->(message, due_at) { 114 | if message.include?("Karen") 115 | raise "Hey Karen your code's broke" 116 | end 117 | } 118 | ) 119 | ``` 120 | 121 | Now, any `TodoOrDie()` invocations in your codebase (other than Karen's) will be 122 | ignored. (You can restore the default hook with `TodoOrDie.reset`). 123 | 124 | ## When is this useful? 125 | 126 | This gem may come in handy whenever you know the code _will_ need to change, 127 | but it can't be changed just yet, and you lack some other reliable means of 128 | ensuring yourself (or your team) will actually follow through on making the 129 | change later. 130 | 131 | This is a good time to recall [LeBlanc's 132 | Law](https://www.quora.com/What-resources-could-I-read-about-Leblancs-law), 133 | which states that `Later == Never`. Countless proofs of this theorem have been 134 | reproduced by software teams around the world. Some common examples: 135 | 136 | * A feature flag was added to the app a long time ago, but the old code path is 137 | still present, even after the flag had been enabled for everyone. Except now 138 | there's also a useless `TODO: delete` comment to keep it company 139 | * A failing test was blocking the build and someone felt an urgent pressure to 140 | deploy the app anyway. So, rather than fix the test, Bill commented it out 141 | "for now" 142 | * You're a real funny guy and you think it'd be hilarious to make a bunch of 143 | Aaron's tests start failing on Christmas morning 144 | 145 | ## Pro-tip 146 | 147 | Cute Rails date helpers are awesome, but don't think you're going to be able to 148 | do this and actually accomplish anything: 149 | 150 | ```ruby 151 | TodoOrDie("Update after APIv2 goes live", 2.weeks.from_now) 152 | ``` 153 | 154 | It will never be two weeks from now. 155 | -------------------------------------------------------------------------------- /test/todo_or_die_test.rb: -------------------------------------------------------------------------------- 1 | class TodoOrDieTest < UnitTest 2 | def test_not_due_todo_does_nothing 3 | Timecop.travel(Date.civil(2200, 2, 3)) 4 | 5 | TodoOrDie("Fix stuff", by: Date.civil(2200, 2, 4)) 6 | 7 | # 🦗 sounds 8 | end 9 | 10 | def test_due_todo_blows_up 11 | Timecop.travel(Date.civil(2200, 2, 4)) 12 | 13 | error = assert_raises(TodoOrDie::OverdueTodo) { 14 | TodoOrDie("Fix stuff", by: Date.civil(2200, 2, 4)) 15 | } 16 | 17 | assert_equal <<~MSG.chomp, error.message 18 | TODO: "Fix stuff" came due on 2200-02-04. Do it! 19 | MSG 20 | end 21 | 22 | def test_warns_when_by_not_passed 23 | Timecop.travel(Date.civil(2200, 2, 4)) 24 | 25 | out, _err = capture_io { 26 | TodoOrDie("Fix stuff", warn_by: Date.civil(2200, 2, 4)) 27 | } 28 | 29 | assert_equal <<~MSG.chomp, out.strip 30 | TODO: "Fix stuff". Don't forget! 31 | MSG 32 | end 33 | 34 | def test_warns_with_by 35 | Timecop.travel(Date.civil(2200, 2, 4)) 36 | 37 | out, _err = capture_io { 38 | TodoOrDie("Fix stuff", by: Date.civil(2200, 2, 5), warn_by: Date.civil(2200, 2, 4)) 39 | } 40 | 41 | assert_equal <<~MSG.chomp, out.strip 42 | TODO: "Fix stuff" is due on 2200-02-05. Don't forget! 43 | MSG 44 | end 45 | 46 | def test_doesnt_warn_early 47 | Timecop.travel(Date.civil(2200, 2, 3)) 48 | 49 | out, _err = capture_io { 50 | TodoOrDie("Fix stuff", by: Date.civil(2200, 2, 5), warn_by: Date.civil(2200, 2, 4)) 51 | } 52 | 53 | assert_equal "", out.strip 54 | end 55 | 56 | def test_config_warn 57 | Timecop.travel(Date.civil(2200, 2, 5)) 58 | actual_message, actual_by = nil 59 | TodoOrDie.config( 60 | warn: ->(message, by) { 61 | actual_message = message 62 | actual_by = by 63 | "pants" 64 | } 65 | ) 66 | some_time = Time.parse("2200-02-06") 67 | some_earlier_time = Time.parse("2200-02-03") 68 | 69 | result = TodoOrDie("kaka", by: some_time, warn_by: some_earlier_time) 70 | 71 | assert_equal result, "pants" 72 | assert_equal actual_message, "kaka" 73 | assert_equal actual_by, some_time 74 | end 75 | 76 | def test_config_custom_explosion 77 | Timecop.travel(Date.civil(2200, 2, 5)) 78 | actual_message, actual_by = nil 79 | TodoOrDie.config( 80 | die: ->(message, by) { 81 | actual_message = message 82 | actual_by = by 83 | "pants" 84 | } 85 | ) 86 | some_time = Time.parse("2200-02-04") 87 | 88 | result = TodoOrDie("kaka", by: some_time) 89 | 90 | assert_equal result, "pants" 91 | assert_equal actual_message, "kaka" 92 | assert_equal actual_by, some_time 93 | end 94 | 95 | def test_config_custom_0_arg_die_callable 96 | Timecop.travel(Date.civil(2200, 2, 5)) 97 | TodoOrDie.config( 98 | die: -> { 99 | :neat 100 | } 101 | ) 102 | 103 | result = TodoOrDie(nil, by: "2200-02-04") 104 | 105 | assert_equal result, :neat 106 | end 107 | 108 | def test_config_custom_1_arg_die_callable 109 | Timecop.travel(Date.civil(2200, 2, 5)) 110 | actual_message = nil 111 | TodoOrDie.config( 112 | die: ->(message) { 113 | actual_message = message 114 | :cool 115 | } 116 | ) 117 | some_time = Time.parse("2200-02-04") 118 | 119 | result = TodoOrDie("secret", by: some_time) 120 | 121 | assert_equal result, :cool 122 | assert_equal actual_message, "secret" 123 | end 124 | 125 | def test_config_and_reset 126 | some_lambda = -> {} 127 | TodoOrDie.config(die: some_lambda) 128 | 129 | assert_equal TodoOrDie.config[:die], some_lambda 130 | assert_equal TodoOrDie.config({})[:die], some_lambda 131 | 132 | TodoOrDie.reset 133 | 134 | assert_equal TodoOrDie.config[:die], TodoOrDie::DEFAULT_CONFIG[:die] 135 | end 136 | 137 | def test_when_rails_is_a_thing_and_not_production 138 | make_it_be_rails(false) 139 | 140 | Timecop.travel(Date.civil(1980, 1, 20)) 141 | 142 | assert_raises(TodoOrDie::OverdueTodo) { 143 | TodoOrDie("I am in Rails", by: Date.civil(1980, 1, 15)) 144 | } 145 | end 146 | 147 | def test_when_rails_is_a_thing_and_is_production 148 | faux_logger = make_it_be_rails(true) 149 | 150 | Timecop.travel(Date.civil(1980, 1, 20)) 151 | 152 | TodoOrDie("Solve the Iranian hostage crisis", by: Date.civil(1980, 1, 20)) 153 | 154 | assert_equal <<~MSG.chomp, faux_logger.warning 155 | TODO: "Solve the Iranian hostage crisis" came due on 1980-01-20. Do it! 156 | MSG 157 | end 158 | 159 | def test_warn_when_rails_is_a_thing 160 | faux_logger = make_it_be_rails(true) 161 | 162 | Timecop.travel(Date.civil(2200, 2, 4)) 163 | 164 | TodoOrDie("Fix stuff", by: Date.civil(2200, 2, 5), warn_by: Date.civil(2200, 2, 4)) 165 | 166 | assert_equal <<~MSG.chomp, faux_logger.warning 167 | TODO: "Fix stuff" is due on 2200-02-05. Don't forget! 168 | MSG 169 | end 170 | 171 | def test_todo_or_die_file_path_removed_from_backtrace 172 | Timecop.travel(Date.civil(2200, 2, 4)) 173 | 174 | error = assert_raises(TodoOrDie::OverdueTodo) { 175 | TodoOrDie("Fix stuff", by: Date.civil(2200, 2, 4)) 176 | } 177 | 178 | assert_empty(error.backtrace.select { |line| line.match?(/todo_or_die\.rb/) }) 179 | end 180 | 181 | def test_has_version 182 | assert TodoOrDie::VERSION 183 | end 184 | 185 | def test_by_string_due_blows_up 186 | Timecop.travel(Date.civil(2200, 2, 4)) 187 | 188 | assert_raises(TodoOrDie::OverdueTodo) { 189 | TodoOrDie("Feelin' stringy", by: "2200-02-04") 190 | } 191 | end 192 | 193 | def test_by_string_not_due_does_not_blow_up 194 | Timecop.travel(Date.civil(2100, 2, 4)) 195 | 196 | TodoOrDie("Feelin' stringy", by: "2200-02-04") 197 | 198 | # 🦗 sounds 199 | end 200 | 201 | def test_due_when_no_by_or_if_is_passed 202 | Timecop.travel(Date.civil(2200, 2, 4)) 203 | 204 | assert_raises(TodoOrDie::OverdueTodo) { 205 | TodoOrDie("Check your math") 206 | } 207 | end 208 | 209 | def test_due_and_if_condition_is_true_blows_up 210 | Timecop.travel(Date.civil(2200, 2, 4)) 211 | 212 | assert_raises(TodoOrDie::OverdueTodo) { 213 | TodoOrDie("Check your math", by: Date.civil(2200, 2, 4), if: -> { 2 + 2 == 4 }) 214 | } 215 | end 216 | 217 | def test_not_due_and_if_condition_is_true_does_not_blow_up 218 | Timecop.travel(Date.civil(2100, 2, 4)) 219 | 220 | TodoOrDie("Check your math", by: Date.civil(2200, 2, 4), if: -> { 2 + 2 == 4 }) 221 | 222 | # 🦗 sounds 223 | end 224 | 225 | def test_due_and_if_condition_is_false_does_not_blow_up 226 | Timecop.travel(Date.civil(2200, 2, 4)) 227 | 228 | TodoOrDie("Check your math", by: Date.civil(2200, 2, 4), if: -> { 2 + 2 == 5 }) 229 | 230 | # 🦗 sounds 231 | end 232 | 233 | def test_by_not_passed_and_if_condition_is_true_blows_up 234 | error = assert_raises(TodoOrDie::OverdueTodo) { 235 | TodoOrDie("Check your math", if: -> { 2 + 2 == 4 }) 236 | } 237 | 238 | assert_equal <<~MSG.chomp, error.message 239 | TODO: "Check your math" has met the conditions to be acted upon. Do it! 240 | MSG 241 | end 242 | 243 | def test_by_and_if_condition_both_true_prints_full_message 244 | error = assert_raises(TodoOrDie::OverdueTodo) { 245 | TodoOrDie("Stuff", by: "1904-02-03", if: -> { true }) 246 | } 247 | 248 | assert_equal <<~MSG.chomp, error.message 249 | TODO: "Stuff" came due on 1904-02-03 and has met the conditions to be acted upon. Do it! 250 | MSG 251 | end 252 | 253 | def test_no_condition_passed_prints_short_message 254 | error = assert_raises(TodoOrDie::OverdueTodo) { 255 | TodoOrDie("Stuff") 256 | } 257 | 258 | assert_equal <<~MSG.chomp, error.message 259 | TODO: "Stuff". Do it! 260 | MSG 261 | end 262 | 263 | def test_by_not_passed_and_if_condition_false_does_not_blow_up 264 | TodoOrDie("Check your math", if: -> { 2 + 2 == 5 }) 265 | 266 | # 🦗 sounds 267 | end 268 | 269 | def test_by_not_passed_and_if_condition_is_false_boolean_does_not_blow_up 270 | TodoOrDie("Check your math", if: false) 271 | 272 | # 🦗 sounds 273 | end 274 | 275 | def test_by_not_passed_and_if_condition_is_true_boolean_blows_up 276 | assert_raises(TodoOrDie::OverdueTodo) { 277 | TodoOrDie("Check your math", if: true) 278 | } 279 | end 280 | 281 | def test_by_not_passed_and_if_condition_is_truthy_blows_up 282 | assert_raises(TodoOrDie::OverdueTodo) { 283 | TodoOrDie("Check your math", if: 42) 284 | } 285 | end 286 | 287 | def test_by_not_passed_and_if_condition_is_falsy_does_not_blow_up 288 | TodoOrDie("Check your math", if: nil) 289 | 290 | # 🦗 sounds 291 | end 292 | end 293 | --------------------------------------------------------------------------------