From bb5619ef8f12c7a0d839d365aa31f451013987d1 Mon Sep 17 00:00:00 2001 From: Eric Budd Date: Sat, 8 Feb 2020 11:16:47 -0500 Subject: [PATCH] Refactoring and updating code to make automated testing easier --- README.md | 21 +++++ TODO.md | 8 ++ iris.rb | 138 ++++++++++++++++++++++------- tests/iris.messages.json | 4 + tests/iris.messages.json.read | 1 + iris_test.rb => tests/iris_test.rb | 18 ++-- 6 files changed, 152 insertions(+), 38 deletions(-) create mode 100644 tests/iris.messages.json create mode 100644 tests/iris.messages.json.read rename iris_test.rb => tests/iris_test.rb (94%) diff --git a/README.md b/README.md index 319120c..026d4ca 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,27 @@ This command does not enter Iris' interactive mode. --- +### --debug + +This option turns on debug mode. Warnings and errors will be output as the program is used. + +Having these messages constantly appear can be distracting or annoying during regular Iris usage, but are useful when tracking down issues. + +This option works in both interactive and non-interactive mode. + + +--- + +### --test-file/-f + +`iris --test-file junk.messages.iris` + +This option forces Iris to load the specified message file, instead of scanning the `/home` directory. + +This option works in both interactive and non-interactive mode. + +--- + ### --help/-h This command displays a complete list of options that Iris recognizes. diff --git a/TODO.md b/TODO.md index 965a0c6..4eb6f41 100644 --- a/TODO.md +++ b/TODO.md @@ -5,16 +5,20 @@ ## Documentation: In Progress # For 1.0.8 +* Add less-style pagination for long messages +* Add -q/--quiet flag, to create iris message file without user intervention? * Add integration tests * Add ability to run with test iris file * Continue to make loader more durable against corrupted data files * Time to start refactoring! * Health check CLI flag +* Create local copies of replied-to messages to limit tampering # Bugs: * Is `Time.now.utc.iso8601` working as expected? * Exclude user's own messages from "unread" count * Fix message ordering when editing/deleting multiple messages +* Replying implicitly to 24 replied to 6 instead # Tech debt: * Flesh out technical sections @@ -28,6 +32,10 @@ * Split helptext into separate file? # Features: +* Add ability to fully manage/read messages from CLI? +* Add local timezone rendering +* Add pagiation/less for long message lists +* Add "Mark unread" option * Add read-only mode if user doesn't want/can't have message file * Add user muting (~/.iris.muted) * Add message editing diff --git a/iris.rb b/iris.rb index 1343ab8..ebe7f81 100755 --- a/iris.rb +++ b/iris.rb @@ -1,26 +1,54 @@ #!/usr/bin/env ruby -require 'time' require 'base64' require 'digest' -require 'json' require 'etc' +require 'json' require 'readline' +require 'time' # require 'pry' # Only needed for debugging class Config VERSION = '1.0.7' MESSAGE_FILE = "#{ENV['HOME']}/.iris.messages" HISTORY_FILE = "#{ENV['HOME']}/.iris.history" - READ_FILE = "#{ENV['HOME']}/.iris.read" IRIS_SCRIPT = __FILE__ USER = ENV['USER'] || ENV['LOGNAME'] || ENV['USERNAME'] HOSTNAME = `hostname -d`.chomp AUTHOR = "#{USER}@#{HOSTNAME}" + OPTIONS = %w[ + --dump + --help + --interactive + --stats + --test-file + --version + -d + -f + -h + -i + -s + -v + ] + INTERACTIVE_OPTIONS = %w[-i --interactive] + NONINTERACTIVE_OPTIONS = %w[-d --dump -h --help -v --version -s --stats] + NONFILE_OPTIONS = %w[-h --help -v --version] def self.find_files (`ls /home/**/.iris.messages`).split("\n") end + + def self.messagefile_filename + $test_corpus_file || Config::MESSAGE_FILE + end + + def self.readfile_filename + "#{messagefile_filename}.read" + end + + def self.historyfile_filename + "#{messagefile_filename}.history" + end end class String @@ -85,9 +113,14 @@ end class Corpus def self.load - @@corpus = Config.find_files.map { |filepath| IrisFile.load_messages(filepath) }.flatten.sort_by(&:timestamp) - @@topics = @@corpus.select{ |m| m.parent == nil && m.show_me? } + if $test_corpus_file + @@corpus = IrisFile.load_messages + else + @@corpus = Config.find_files.map { |filepath| IrisFile.load_messages(filepath) }.flatten.sort_by(&:timestamp) + end + @@my_corpus = IrisFile.load_messages.sort_by(&:timestamp) + @@topics = @@corpus.select{ |m| m.parent == nil && m.show_me? } @@my_reads = IrisFile.load_reads @@all_hash_to_index = @@corpus.reduce({}) { |agg, msg| agg[msg.hash] = @@corpus.index(msg); agg } @@all_parent_hash_to_index = @@corpus.reduce({}) do |agg, msg| @@ -187,15 +220,17 @@ class Corpus end class IrisFile - def self.load_messages(filepath = Config::MESSAGE_FILE) - # For logger: "Checking #{filepath}" + def self.load_messages(filepath = nil) + if filepath.nil? + filepath = Config.messagefile_filename + end + return [] unless File.exists?(filepath) - # For logger: "Found, parsing #{filepath}..." begin payload = JSON.parse(File.read(filepath)) rescue JSON::ParserError => e - if filepath == Config::MESSAGE_FILE + if filepath == Config.messagefile_filename Display.flowerbox( 'Your message file appears to be corrupt.', "Could not parse valid JSON from #{filepath}", @@ -208,7 +243,7 @@ class IrisFile end unless payload.is_a?(Array) - if filepath == Config::MESSAGE_FILE + if filepath == Config.messagefile_filename Display.flowerbox( 'Your message file appears to be corrupt.', "Could not interpret data from #{filepath}", @@ -232,14 +267,14 @@ class IrisFile end def self.load_reads - return [] unless File.exists? Config::READ_FILE + return [] unless File.exists? Config.readfile_filename begin - read_array = JSON.parse(File.read(Config::READ_FILE)) + read_array = JSON.parse(File.read(Config.readfile_filename)) rescue JSON::ParserError => e Display.flowerbox( 'Your read file appears to be corrupt.', - "Could not parse valid JSON from #{Config::READ_FILE}", + "Could not parse valid JSON from #{Config.readfile_filename}", 'Please fix or delete this read file to use Iris.') exit(1) end @@ -247,7 +282,7 @@ class IrisFile unless read_array.is_a?(Array) Display.flowerbox( 'Your read file appears to be corrupt.', - "Could not interpret data from #{Config::READ_FILE}", + "Could not interpret data from #{Config.readfile_filename}", '(It\'s not a JSON array of message hashes, as far as I can tell)', 'Please fix or delete this read file to use Iris.') exit(1) @@ -257,23 +292,29 @@ class IrisFile end def self.create_message_file + raise 'Should not try to create message file in test mode!' if $test_corpus_file raise 'Message file exists; refusing to overwrite!' if File.exists?(Config::MESSAGE_FILE) File.umask(0122) File.open(Config::MESSAGE_FILE, 'w') { |f| f.write('[]') } end def self.create_read_file - raise 'Read file exists; refusing to overwrite!' if File.exists?(Config::READ_FILE) + return if File.exists?(Config.readfile_filename) + File.umask(0122) - File.open(Config::READ_FILE, 'w') { |f| f.write('[]') } + File.open(Config.readfile_filename, 'w') { |f| f.write('[]') } end def self.write_corpus(corpus) - File.write(Config::MESSAGE_FILE, corpus) + File.write(Config.messagefile_filename, corpus) end def self.write_read_file(new_read_hashes) - File.write(Config::READ_FILE, new_read_hashes) + if $test_corpus_file + File.write("#{$test_corpus_file}.read", new_read_hashes) + else + File.write(Config.readfile_filename, new_read_hashes) + end end end @@ -372,7 +413,7 @@ class Message stub = message.split("\n").first end return stub.colorize if stub.decolorize.length <= length - # colorize the stub, then decolorize to strip out any partial tags + # Colorize the stub, then decolorize to strip out any partial tags stub.colorize.slice(0, length - 5 - Display.topic_index_width).decolorize + '...' end @@ -577,7 +618,7 @@ class Interface cmd = CMD_MAP[cmd] || cmd return self.send(cmd.to_sym) if ONE_SHOTS.include?(cmd) && tokens.length == 1 return show_topic(cmd) if cmd =~ /^\d+$/ - # We must have args, let's handle 'em + # If we've gotten this far, we must have args. Let's handle 'em. arg = tokens.last return reply(arg) if cmd == 'reply' return edit(arg) if cmd == 'edit' @@ -590,11 +631,17 @@ class Interface end def self.info + topic_count = Corpus.topics.size + unread_topic_count = Corpus.unread_topics.size + message_count = Corpus.size + unread_message_count = Corpus.unread_messages.size + author_count = Corpus.all.map(&:author).uniq.size + Display.flowerbox( "Iris #{Config::VERSION}", - "#{Corpus.topics.size} #{'topic'.pluralize(Corpus.topics.size)}, #{Corpus.unread_topics.size} unread.", - "#{Corpus.size} #{'message'.pluralize(Corpus.size)}, #{Corpus.unread_messages.size} unread.", - "#{Corpus.all.map(&:author).uniq.size} authors.", + "#{topic_count} #{'topic'.pluralize(topic_count)}, #{unread_topic_count} unread.", + "#{message_count} #{'message'.pluralize(message_count)}, #{unread_message_count} unread.", + "#{author_count} #{'author'.pluralize(author_count)}.", box_thickness: 0) end @@ -794,7 +841,6 @@ class Interface end def initialize(args) - Corpus.load @history_loaded = false @mode = :browsing @@ -882,6 +928,7 @@ class CLI '--stats, -s - Display Iris version and message stats.', '--interactive, -i - Enter interactive mode (default)', '--dump, -d - Dump entire message corpus out.', + '--test-file , -f - Use the specified test file for messages.', '', 'If no options are provided, Iris will enter interactive mode.', box_character: '') @@ -899,13 +946,11 @@ class CLI end if (args & %w{-s --stats}).any? - Corpus.load Interface.info exit(0) end if (args & %w{-d --dump}).any? - Corpus.load puts Corpus.to_json exit(0) end @@ -917,16 +962,21 @@ end class Startupper def initialize(args) - perform_startup_checks + perform_file_checks unless Config::NONFILE_OPTIONS.include?(args) - if (args & %w{-i --interactive}).any? || args.empty? + load_corpus(args) + + is_interactive = (args & Config::NONINTERACTIVE_OPTIONS).none? || (args & Config::INTERACTIVE_OPTIONS).any? + + if is_interactive Interface.start(args) else CLI.start(args) end end - def perform_startup_checks + def perform_file_checks + raise 'Should not try to perform file checks in test mode!' if $test_corpus_file 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 @@ -939,20 +989,44 @@ class Startupper end end - IrisFile.create_read_file unless File.exists?(Config::READ_FILE) + IrisFile.create_read_file if File.stat(Config::MESSAGE_FILE).mode != 33188 Display.permissions_error(Config::MESSAGE_FILE, 'message', '-rw-r--r--', '644', "Leaving your file with incorrect permissions could allow unauthorized edits!") end - if File.stat(Config::READ_FILE).mode != 33188 - Display.permissions_error(Config::READ_FILE, 'read', '-rw-r--r--', '644') + if File.stat(Config.readfile_filename).mode != 33188 + Display.permissions_error(Config.readfile_filename, 'read', '-rw-r--r--', '644') end if File.stat(Config::IRIS_SCRIPT).mode != 33261 Display.permissions_error(Config::IRIS_SCRIPT, 'Iris', '-rwxr-xr-x', '755', 'If this file has the wrong permissions the program may be tampered with!') end 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] + + unless filename + Display.say "Option `--test-file` (`-f`) expects a filename" + exit(1) + end + + unless File.exist?(filename) + Display.say "Could not load test file: #{filename}" + exit(1) + end + + Display.say "Using Iris with test file: #{filename}" + $test_corpus_file = filename + end + + Corpus.load + end end Startupper.new(ARGV) if __FILE__==$0 diff --git a/tests/iris.messages.json b/tests/iris.messages.json new file mode 100644 index 0000000..f502639 --- /dev/null +++ b/tests/iris.messages.json @@ -0,0 +1,4 @@ +[ + {"hash":"gpY2WW/jGcH+BODgySCwDANJlIM=\n","edit_hash":null,"is_deleted":null,"data":{"author":"calamitous@ctrl-c.club","parent":null,"timestamp":"2020-01-07T21:04:21Z","message":"Test"}}, + {"hash":"qubS6AvNXgCJj/4ClFocuJ16SBk=\n","edit_hash":null,"is_deleted":null,"data":{"author":"calamitous@ctrl-c.club","parent":"gpY2WW/jGcH+BODgySCwDANJlIM=\n","timestamp":"2020-01-07T21:32:30Z","message":"Wat"}} +] \ No newline at end of file diff --git a/tests/iris.messages.json.read b/tests/iris.messages.json.read new file mode 100644 index 0000000..575d36d --- /dev/null +++ b/tests/iris.messages.json.read @@ -0,0 +1 @@ +["gpY2WW/jGcH+BODgySCwDANJlIM=\n","qubS6AvNXgCJj/4ClFocuJ16SBk=\n"] \ No newline at end of file diff --git a/iris_test.rb b/tests/iris_test.rb similarity index 94% rename from iris_test.rb rename to tests/iris_test.rb index 688bebc..8b10975 100644 --- a/iris_test.rb +++ b/tests/iris_test.rb @@ -35,7 +35,7 @@ describe Config do describe '.find_files' do it 'looks up all the Iris message files on the system' do - # I am so sorry + # I am so sorry about this `expects` clause Config.expects(:`).with('ls /home/**/.iris.messages').returns('') Config.find_files end @@ -230,11 +230,17 @@ describe Startupper do let(:bad_file_stat) { a = mock; a.stubs(:mode).returns(2); a } before do - Config.send(:remove_const, 'MESSAGE_FILE') - Config.send(:remove_const, 'READ_FILE') - Config.send(:remove_const, 'IRIS_SCRIPT') - Config::MESSAGE_FILE = message_file_path - Config::READ_FILE = read_file_path + Config.stubs(:find_files).returns([]) + IrisFile.stubs(:load_messages).returns([]) + IrisFile.stubs(:load_reads).returns([]) + + Config.send(:remove_const, 'MESSAGE_FILE') if Config.const_defined? 'MESSAGE_FILE' + Config.send(:remove_const, 'READ_FILE') if Config.const_defined? 'READ_FILE' + Config.send(:remove_const, 'IRIS_SCRIPT') if Config.const_defined? 'IRIS_SCRIPT' + Config::MESSAGE_FILE = message_file_path + Config::READ_FILE = read_file_path + Config.stubs(:messagefile_filename).returns(message_file_path) + Config.stubs(:readfile_filename).returns(read_file_path) Config::IRIS_SCRIPT = 'doots' File.stubs(:exists?).returns(true)