diff --git a/README.md b/README.md index b6dc374..5aa63fc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Iris is strictly text-based, requiring no GUI or web servers. ## Installation -At its core, Iris is simply a single, executable Ruby script. It has been tested and is known to work with Ruby 2.3.5. No extra gems or libraries are required. +At its core, Iris is simply a single, executable Ruby script. It has been tested and is known to work with Ruby 2.3.5 and above. No extra gems or libraries are required. Copy or symlink `iris.rb` somewhere the whole server can use it; `/usr/local/bin` is a good candidate: @@ -616,6 +616,12 @@ gem install --user-install minitest gem install --user-install mocha ``` +To run the tests: + +```bash +ruby tests/iris_test.rb +``` + ## Technical Bits * [Dependencies](#dependencies) diff --git a/iris.rb b/iris.rb index ad6c974..ef912ac 100755 --- a/iris.rb +++ b/iris.rb @@ -1037,7 +1037,7 @@ class Startupper NONFILE_OPTIONS = %w[-h --help -v --version] def initialize(args) - perform_file_checks unless NONFILE_OPTIONS.include?(args) + Startupper.perform_file_checks unless NONFILE_OPTIONS.include?(args) load_corpus(args) @@ -1052,8 +1052,7 @@ class Startupper end end - def perform_file_checks - raise 'Should not try to perform file checks in test mode!' if $test_corpus_file + def self.perform_file_checks unless File.exists?(Config::MESSAGE_FILE) Display.say "You don't have a message file at #{Config::MESSAGE_FILE}." response = Readline.readline 'Would you like me to create it for you? (y/n) ', true @@ -1082,8 +1081,6 @@ class Startupper end def load_corpus(args) - $test_corpus_file = nil - if (args & %w{-f --test-file}).any? filename_idx = (args.index('-f') || args.index('--test-file')) + 1 filename = args[filename_idx] diff --git a/tests/iris_test.rb b/tests/iris_test.rb index 3725ac5..b37b9ad 100644 --- a/tests/iris_test.rb +++ b/tests/iris_test.rb @@ -1,37 +1,37 @@ require 'minitest/autorun' -require 'mocha/mini_test' +require 'mocha/minitest' +# This allows the test to pretend that user "jerryberry" is logged in. +ENV['USER'] = 'jerryberry' + +# Set this before loading the code so that the Config constants load correctly. $test_corpus_file = "./tests/iris.messages.json" -# Setting this before loading the main code file so that the Config contants -# load correctly. This will allows the test to pretend that user "jerryberry" -# is logged in. -ENV.stubs(:[]).returns('jerryberry') require './iris.rb' describe Config do it 'has the Iris semantic version number' do - Config::VERSION.must_match /^\d\.\d\.\d+$/ + _(Config::VERSION).must_match /^\d\.\d\.\d+$/ end it 'has the message file location' do - Config::MESSAGE_FILE.must_match /\/\.iris\.messages$/ + _(Config::MESSAGE_FILE).must_match /\/\.iris\.messages$/ end it 'has the readline history file location' do - Config::HISTORY_FILE.must_match /\/\.iris\.history$/ + _(Config::HISTORY_FILE).must_match /\/\.iris\.history$/ end it 'has the username' do - Config::USER.must_equal 'jerryberry' + _(Config::USER).must_equal 'jerryberry' end it 'has a hostname' do - Config::HOSTNAME.wont_be_nil + _(Config::HOSTNAME).wont_be_nil end it 'has the author' do - Config::AUTHOR.must_equal "#{Config::USER}@#{Config::HOSTNAME}" + _(Config::AUTHOR).must_equal "#{Config::USER}@#{Config::HOSTNAME}" end describe '.find_files' do @@ -43,17 +43,21 @@ describe Config do it 'returns a list of Iris message files' do Config.stubs(:`).returns("foo\nbar\n") - Config.find_files.must_equal ['foo', 'bar'] + _(Config.find_files).must_equal ['foo', 'bar'] end it 'returns an empty array if no Iris message files are found' do Config.stubs(:`).returns('') - Config.find_files.must_equal [] + _(Config.find_files).must_equal [] end end end describe Corpus do + before do + Corpus.load + end + describe '.load' do it 'loads all the message files' it 'sets the corpus class variable' @@ -62,10 +66,6 @@ describe Corpus do it 'creates parent-hash-to-child-indexes index' end - describe '.all' do - it 'returns the entire corpus of messages' - end - describe '.topics' do it 'returns all the messages which are topics' it 'does not return reply messages' @@ -78,28 +78,26 @@ describe Corpus do describe '.find_message_by_hash' do it 'returns nil if a nil is passed in' do - Corpus.find_message_by_hash(nil).must_equal nil + assert_nil Corpus.find_message_by_hash(nil) end it 'returns nil if the hash is not found in the corpus' do - # Corpus.load - Corpus.find_message_by_hash('NoofMcGoof').must_equal nil + assert_nil Corpus.find_message_by_hash('NoofMcGoof') end it 'returns the message associated with the hash if it is found' do - Corpus.load - Corpus.find_message_by_hash("gpY2WW/jGcH+BODgySCwDANJlIM=").must_equal "Test" + message = Corpus.find_message_by_hash("gpY2WW/jGcH+BODgySCwDANJlIM=\n") + _(message.message).must_equal "Test" end end describe '.find_all_by_parent_hash' do it 'returns an empty array if a nil is passed in' do - Corpus.find_all_by_parent_hash(nil).must_equal [] + _(Corpus.find_all_by_parent_hash(nil)).must_equal [] end it 'returns an empty array if the hash is not a parent of any other messages' do - skip - Corpus.find_all_by_parent_hash('GoofMcDoof').must_equal [] + _(Corpus.find_all_by_parent_hash('GoofMcDoof')).must_equal [] end it 'returns an empty array if the hash is not found in the corpus' @@ -108,7 +106,7 @@ describe Corpus do describe '.find_topic_by_id' do it 'returns nil if a nil is passed in' do - Corpus.find_topic_by_id(nil).must_equal nil + assert_nil Corpus.find_topic_by_id(nil) end describe 'when an index string is passed in' do @@ -119,7 +117,7 @@ describe Corpus do describe '.find_topic_by_hash' do it 'returns nil if a nil is passed in' do - Corpus.find_topic_by_hash(nil).must_equal nil + assert_nil Corpus.find_topic_by_hash(nil) end describe 'when a hash string is passed in' do @@ -135,10 +133,6 @@ describe IrisFile do end describe Message do - it 'has a file version' do - Message::FILE_FORMAT.must_match /v\d/ - end - it 'exposes all its data attributes for reading' it 'is #valid? if it has no errors' @@ -163,33 +157,44 @@ end describe Display do it 'has a setting for a minimum width of 80' do - Display::MIN_WIDTH.must_equal 80 + _(Display::MIN_WIDTH).must_equal 80 end - it 'has a setting for the calculated screen width' + it 'has a setting for a minimum height of 8' do + _(Display::MIN_HEIGHT).must_equal 8 + end + + it 'has settings for the calculated screen geometry' do + _(Display::WIDTH).wont_equal nil + _(Display::HEIGHT).wont_equal nil + end describe '#topic_index_width' do it 'returns the a minimun length of 2' do Corpus.stubs(:topics).returns(%w{a}) - Display.topic_index_width.must_equal 2 + _(Display.topic_index_width).must_equal 2 end it 'returns the length in characters of the longest topic index' do Corpus.stubs(:topics).returns((0..1000).to_a) - Display.topic_index_width.must_equal 4 + _(Display.topic_index_width).must_equal 4 end it 'returns 2 if there are no topics' do Corpus.stubs(:topics).returns([]) - Display.topic_index_width.must_equal 2 + _(Display.topic_index_width).must_equal 2 end end describe '#topic_author_width' do - it 'returns the length in characters of the longest author\'s name' + it 'returns the length in characters of the longest author\'s name' do + Corpus.stubs(:authors).returns(['jerryberry@ctrl-c.club']) + _(Display.topic_author_width).must_equal 22 + end + it 'returns 1 if there are no topics' do - Corpus.stubs(:topics).returns([]) - Display.topic_author_width.must_equal 1 + Corpus.stubs(:authors).returns([]) + _(Display.topic_author_width).must_equal 1 end end @@ -254,46 +259,6 @@ describe Startupper do File.stubs(:stat).with(read_file_path).returns(data_file_stat) Interface.stubs(:start) - Display.stubs(:say) - end - - it 'offers to create a message file if the user doesn\'t have one' do - File.stubs(:exists?).with(message_file_path).returns(false) - Readline.expects(:readline).with('Would you like me to create it for you? (y/n) ', true).returns('y') - IrisFile.expects(:create_message_file) - - Startupper.new([]) - end - - it 'creates a read file if the user doesn\'t have one' do - File.stubs(:exists?).with(read_file_path).returns(false) - IrisFile.expects(:create_read_file) - - Startupper.new([]) - end - - it 'warns the user if the message file permissions are wrong' do - skip - File.expects(:stat).with(message_file_path).returns(bad_file_stat) - Display.expects(:say).with('Your message file has incorrect permissions! Should be "-rw-r--r--".') - - Startupper.new([]) - end - - it 'warns the user if the read file permissions are wrong' do - skip - File.stubs(:stat).with(read_file_path).returns(bad_file_stat) - Display.expects(:say).with('Your read file has incorrect permissions! Should be "-rw-r--r--".') - - Startupper.new([]) - end - - it 'warns the user if the script file permissions are wrong' do - skip - File.expects(:stat).with(Config::IRIS_SCRIPT).returns(bad_file_stat) - Display.expects(:say).with('The Iris file has incorrect permissions! Should be "-rwxr-xr-x".') - - Startupper.new([]) end it 'starts the Interface if no command-line arguments are provided' do @@ -315,11 +280,67 @@ describe Startupper do CLI.expects(:start).with(['-h']) Startupper.new(['-h']) end + + it 'offers to create a message file if the user doesn\'t have one' do + File.stubs(:exists?).with(message_file_path).returns(false) + Display.stubs(:say) + Readline.expects(:readline).with('Would you like me to create it for you? (y/n) ', true).returns('y') + IrisFile.expects(:create_message_file) + + Startupper.new([]) + end + + it 'creates a read file if the user doesn\'t have one' do + File.stubs(:exists?).with(read_file_path).returns(false) + IrisFile.expects(:create_read_file) + + Startupper.new([]) + end + + it 'warns the user if the message file permissions are wrong' do + File.expects(:stat).with(message_file_path).returns(bad_file_stat) + Display.stubs(:say) + message_lines = [ + "Your message file has incorrect permissions! Should be \"-rw-r--r--\".", + "You can change this from the command line with:", + " chmod 644 jerryberry/.iris.messages", + "Leaving your file with incorrect permissions could allow unauthorized edits!" + ] + Display.expects(:say).with(message_lines) + + Startupper.new([]) + end + + it 'warns the user if the read file permissions are wrong' do + File.stubs(:stat).with(read_file_path).returns(bad_file_stat) + Display.stubs(:say) + message_lines = [ + "Your read file has incorrect permissions! Should be \"-rw-r--r--\".", + "You can change this from the command line with:", + " chmod 644 jerryberry/.iris.read" + ] + Display.expects(:say).with(message_lines) + + Startupper.new([]) + end + + it 'warns the user if the script file permissions are wrong' do + File.expects(:stat).with(Config::IRIS_SCRIPT).returns(bad_file_stat) + Display.stubs(:say) + message_lines = [ + "Your Iris file has incorrect permissions! Should be \"-rwxr-xr-x\".", + "You can change this from the command line with:", + " chmod 755 doots", "If this file has the wrong permissions the program may be tampered with!" + ] + Display.expects(:say).with(message_lines) + + Startupper.new([]) + end end end describe 'String#colorize' do - let(:color_string) { + let(:color_strings) { " RED {r normal}\t{ri intense}\t{ru underline}\t{riu intense underline} {rv reverse}\t{riv intense}\t{ruv underline}\t{riuv intense underline} @@ -335,28 +356,59 @@ describe 'String#colorize' do {cv reverse}\t{civ intense}\t{cuv underline}\t{ciuv intense underline} WHITE {w normal}\t{wi intense}\t{wu underline}\t{wiu intense underline} {wv reverse}\t{wiv intense}\t{wuv underline}\t{wiuv intense underline} - " + ".split("\n")[1..-2] } it 'produces the expected output' do - skip - # color_string.split("\n")[1].colorize.must_equal "\n RED \e[31mnormal\e[0m\t\e[1;31mintense\e[0m\t\e[31;4munderline\e[0m\t\e[1;31;4mintense underline\e[0m\n \e[31;7mreverse\e[0m\t\e[1;31;7mintense\e[0m\t\e[31;4;7munderline\e[0m\t\e[1;31;4;7mintense underline\e[0m\n GREEN \e[32mnormal\e[0m\t\e[1;32mintense\e[0m\t\e[32;4munderline\e[0m\t\e[1;32;4mintense underline\e[0m\n \e[32;7mreverse\e[0m\t\e[1;32;7mintense\e[0m\t\e[32;4;7munderline\e[0m\t\e[1;32;4;7mintense underline\e[0m\n YELLOW \e[33mnormal\e[0m\t\e[1;33mintense\e[0m\t\e[33;4munderline\e[0m\t\e[1;33;4mintense underline\e[0m\n \e[33;7mreverse\e[0m\t\e[1;33;7mintense\e[0m\t\e[33;4;7munderline\e[0m\t\e[1;33;4;7mintense underline\e[0m\n BLUE \e[34mnormal\e[0m\t\e[1;34mintense\e[0m\t\e[34;4munderline\e[0m\t\e[1;34;4mintense underline\e[0m\n \e[34;7mreverse\e[0m\t\e[1;34;7mintense\e[0m\t\e[34;4;7munderline\e[0m\t\e[1;34;4;7mintense underline\e[0m\n MAGENTA \e[35mnormal\e[0m\t\e[1;35mintense\e[0m\t\e[35;4munderline\e[0m\t\e[1;35;4mintense underline\e[0m\n \e[35;7mreverse\e[0m\t\e[1;35;7mintense\e[0m\t\e[35;4;7munderline\e[0m\t\e[1;35;4;7mintense underline\e[0m\n CYAN \e[36mnormal\e[0m\t\e[1;36mintense\e[0m\t\e[36;4munderline\e[0m\t\e[1;36;4mintense underline\e[0m\n \e[36;7mreverse\e[0m\t\e[1;36;7mintense\e[0m\t\e[36;4;7munderline\e[0m\t\e[1;36;4;7mintense underline\e[0m\n WHITE \e[37mnormal\e[0m\t\e[1;37mintense\e[0m\t\e[37;4munderline\e[0m\t\e[1;37;4mintense underline\e[0m\n \e[37;7mreverse\e[0m\t\e[1;37;7mintense\e[0m\t\e[37;4;7munderline\e[0m\t\e[1;37;4;7mintense underline\e[0m\n \e[0m" - color_string.split("\n")[1].colorize.must_equal " RED \e[31mnormal\e[0m\t\e[1;31mintense\e[0m\t\e[31;4munderline\e[0m\t\e[1;31;4mintense underline\e[0m\e[0m" + lead = "\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m " + lines = [ + "RED \e[31mnormal\e[0m\t\e[1;31mintense\e[0m\t\e[31;4munderline\e[0m\t\e[1;31;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[31;7mreverse\e[0m\t\e[1;31;7mintense\e[0m\t\e[31;4;7munderline\e[0m\t\e[1;31;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + "GREEN \e[32mnormal\e[0m\t\e[1;32mintense\e[0m\t\e[32;4munderline\e[0m\t\e[1;32;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[32;7mreverse\e[0m\t\e[1;32;7mintense\e[0m\t\e[32;4;7munderline\e[0m\t\e[1;32;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + "YELLOW \e[33mnormal\e[0m\t\e[1;33mintense\e[0m\t\e[33;4munderline\e[0m\t\e[1;33;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[33;7mreverse\e[0m\t\e[1;33;7mintense\e[0m\t\e[33;4;7munderline\e[0m\t\e[1;33;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + "BLUE \e[34mnormal\e[0m\t\e[1;34mintense\e[0m\t\e[34;4munderline\e[0m\t\e[1;34;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[34;7mreverse\e[0m\t\e[1;34;7mintense\e[0m\t\e[34;4;7munderline\e[0m\t\e[1;34;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + "MAGENTA \e[35mnormal\e[0m\t\e[1;35mintense\e[0m\t\e[35;4munderline\e[0m\t\e[1;35;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[35;7mreverse\e[0m\t\e[1;35;7mintense\e[0m\t\e[35;4;7munderline\e[0m\t\e[1;35;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + "CYAN \e[36mnormal\e[0m\t\e[1;36mintense\e[0m\t\e[36;4munderline\e[0m\t\e[1;36;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[36;7mreverse\e[0m\t\e[1;36;7mintense\e[0m\t\e[36;4;7munderline\e[0m\t\e[1;36;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + "WHITE \e[37mnormal\e[0m\t\e[1;37mintense\e[0m\t\e[37;4munderline\e[0m\t\e[1;37;4mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + " \e[37;7mreverse\e[0m\t\e[1;37;7mintense\e[0m\t\e[37;4;7munderline\e[0m\t\e[1;37;4;7mintense underline\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m\e[0m", + ] + + _(color_strings[0].colorize).must_equal lead + lines[0] + _(color_strings[1].colorize).must_equal lead + lines[1] + _(color_strings[2].colorize).must_equal lead + lines[2] + _(color_strings[3].colorize).must_equal lead + lines[3] + _(color_strings[4].colorize).must_equal lead + lines[4] + _(color_strings[5].colorize).must_equal lead + lines[5] + _(color_strings[6].colorize).must_equal lead + lines[6] + _(color_strings[7].colorize).must_equal lead + lines[7] + _(color_strings[8].colorize).must_equal lead + lines[8] + _(color_strings[9].colorize).must_equal lead + lines[9] + _(color_strings[10].colorize).must_equal lead + lines[10] + _(color_strings[11].colorize).must_equal lead + lines[11] + _(color_strings[12].colorize).must_equal lead + lines[12] + _(color_strings[13].colorize).must_equal lead + lines[13] end it 'returns an empty string wrapped with resets when provided an empty string' do - ''.colorize.must_equal "\e[0m\e[0m" + _(''.colorize).must_equal "\e[0m\e[0m" end it 'allows curly brackets to be escaped' do - 'I want \{no color\}'.colorize.must_equal "\e[0m\e[0mI want {no color}\e[0m\e[0m\e[0m" + _('I want \{no color\}'.colorize).must_equal "\e[0m\e[0mI want {no color}\e[0m\e[0m\e[0m" end end describe 'String#decolorize' do - it 'returns the string with the coloring tags stripped' + it 'returns the string with the coloring tags stripped' do + _("{b colorful}".decolorize).must_equal "colorful" + end it 'allows curly brackets to be escaped' do - 'I want \{no color\}'.decolorize.must_equal "I want {no color}" + _('I want \{no color\}'.decolorize).must_equal "I want {no color}" end end