markdown_exec 2.8.3 → 2.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +41 -0
 - data/Gemfile.lock +1 -1
 - data/Rakefile +33 -23
 - data/bats/{block-types.bats → block-type-bash.bats} +0 -25
 - data/bats/block-type-link.bats +9 -0
 - data/bats/block-type-port.bats +16 -0
 - data/bats/block-type-ux-allowed.bats +29 -0
 - data/bats/block-type-ux-auto.bats +1 -1
 - data/bats/block-type-ux-chained.bats +9 -0
 - data/bats/block-type-ux-default.bats +8 -0
 - data/bats/block-type-ux-echo-hash.bats +14 -0
 - data/bats/block-type-ux-echo.bats +2 -2
 - data/bats/block-type-ux-exec.bats +1 -1
 - data/bats/block-type-ux-hidden.bats +9 -0
 - data/bats/block-type-ux-invalid.bats +8 -0
 - data/bats/block-type-ux-transform.bats +1 -1
 - data/bats/indented-block-type-vars.bats +9 -0
 - data/bats/line-decor-dynamic.bats +1 -1
 - data/bats/test_helper.bash +9 -2
 - data/bats/variable-expansion-multiline.bats +8 -0
 - data/bats/variable-expansion.bats +1 -1
 - data/docs/dev/block-type-ux-allowed.md +82 -0
 - data/docs/dev/block-type-ux-auto.md +2 -1
 - data/docs/dev/block-type-ux-chained.md +21 -0
 - data/docs/dev/block-type-ux-default.md +42 -0
 - data/docs/dev/block-type-ux-echo-hash.md +78 -0
 - data/docs/dev/block-type-ux-echo.md +3 -1
 - data/docs/dev/block-type-ux-exec.md +1 -0
 - data/docs/dev/block-type-ux-hidden.md +21 -0
 - data/docs/dev/block-type-ux-invalid.md +5 -0
 - data/docs/dev/block-type-ux-require.md +9 -18
 - data/docs/dev/indented-block-type-vars.md +6 -0
 - data/docs/dev/line-decor-dynamic.md +2 -1
 - data/docs/dev/variable-expansion-multiline.md +31 -0
 - data/lib/ansi_formatter.rb +0 -1
 - data/lib/ansi_string.rb +1 -1
 - data/lib/array.rb +0 -1
 - data/lib/array_util.rb +0 -1
 - data/lib/block_label.rb +1 -1
 - data/lib/cached_nested_file_reader.rb +1 -1
 - data/lib/constants.rb +18 -0
 - data/lib/directory_searcher.rb +1 -1
 - data/lib/exceptions.rb +0 -1
 - data/lib/fcb.rb +52 -9
 - data/lib/filter.rb +1 -2
 - data/lib/format_table.rb +1 -0
 - data/lib/fout.rb +1 -1
 - data/lib/hash.rb +0 -1
 - data/lib/hash_delegator.rb +404 -224
 - data/lib/link_history.rb +17 -17
 - data/lib/logged_struct.rb +429 -0
 - data/lib/markdown_exec/version.rb +1 -1
 - data/lib/markdown_exec.rb +3 -3
 - data/lib/mdoc.rb +21 -31
 - data/lib/menu.src.yml +15 -7
 - data/lib/menu.yml +11 -6
 - data/lib/namer.rb +3 -6
 - data/lib/null_result.rb +131 -0
 - data/lib/object_present.rb +1 -1
 - data/lib/option_value.rb +1 -1
 - data/lib/resize_terminal.rb +1 -2
 - data/lib/saved_assets.rb +1 -1
 - data/lib/saved_files_matcher.rb +1 -1
 - data/lib/shell_session.rb +439 -0
 - data/lib/streams_out.rb +0 -1
 - data/lib/string_util.rb +11 -1
 - data/lib/success_result.rb +112 -0
 - data/lib/text_analyzer.rb +1 -0
 - data/lib/ww.rb +9 -7
 - metadata +25 -3
 
| 
         @@ -0,0 +1,439 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env -S bundle exec ruby -r./lib/ww
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            # encoding=utf-8
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require 'open3'
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            STATUS_SUCCESS = 0
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            # ShellSession provides an interactive bash session to execute commands,
         
     | 
| 
      
 11 
     | 
    
         
            +
            # capturing both standard output and standard error separately with timestamps.
         
     | 
| 
      
 12 
     | 
    
         
            +
            # Each output line is stored as a hash containing its text and the time it was received.
         
     | 
| 
      
 13 
     | 
    
         
            +
            class ShellSession
         
     | 
| 
      
 14 
     | 
    
         
            +
              attr_reader :exitstatus, :output, :lines, :stdout, :stderr, :stdout_lines,
         
     | 
| 
      
 15 
     | 
    
         
            +
                          :stderr_lines, :waiting_for_input
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              def initialize(setup_command = nil)
         
     | 
| 
      
 18 
     | 
    
         
            +
                # Open a persistent bash session with separate stdout and stderr streams.
         
     | 
| 
      
 19 
     | 
    
         
            +
                @stdin, @stdout_stream, @stderr_stream, @wait_thr = Open3.popen3('bash')
         
     | 
| 
      
 20 
     | 
    
         
            +
                @stdin.flush
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                # Random boundary marker between commands.
         
     | 
| 
      
 23 
     | 
    
         
            +
                @boundary = Random.new.rand.to_s
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                # Line sequence counter
         
     | 
| 
      
 26 
     | 
    
         
            +
                @line_counter = 0
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                # Most recent command.
         
     | 
| 
      
 29 
     | 
    
         
            +
                @command = ''
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                # Buffers for captured output.
         
     | 
| 
      
 32 
     | 
    
         
            +
                @stdout_lines = []
         
     | 
| 
      
 33 
     | 
    
         
            +
                @stderr_lines = []
         
     | 
| 
      
 34 
     | 
    
         
            +
                @output = ''
         
     | 
| 
      
 35 
     | 
    
         
            +
                @lines = []
         
     | 
| 
      
 36 
     | 
    
         
            +
                @stdout = ''
         
     | 
| 
      
 37 
     | 
    
         
            +
                @stderr = ''
         
     | 
| 
      
 38 
     | 
    
         
            +
                @waiting_for_input = false
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                return @exitstatus = STATUS_SUCCESS if setup_command.nil? || setup_command.strip.empty?
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                run_command(setup_command)
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
              # Executes a command in the bash session and returns its output.
         
     | 
| 
      
 46 
     | 
    
         
            +
              # Stdout and stderr are captured separately with a timestamp for each line.
         
     | 
| 
      
 47 
     | 
    
         
            +
              #
         
     | 
| 
      
 48 
     | 
    
         
            +
              # @param command [String] The command to execute.
         
     | 
| 
      
 49 
     | 
    
         
            +
              # @param interactive [Boolean] Whether this command expects user input
         
     | 
| 
      
 50 
     | 
    
         
            +
              # @return [Hash] A hash containing :status, :output (stdout), :stdout, and :stderr.
         
     | 
| 
      
 51 
     | 
    
         
            +
              def run_command(command, interactive = false)
         
     | 
| 
      
 52 
     | 
    
         
            +
                @exitstatus = STATUS_SUCCESS
         
     | 
| 
      
 53 
     | 
    
         
            +
                @command = command
         
     | 
| 
      
 54 
     | 
    
         
            +
                @waiting_for_input = false
         
     | 
| 
      
 55 
     | 
    
         
            +
                return { output: '', status: STATUS_SUCCESS,
         
     | 
| 
      
 56 
     | 
    
         
            +
                         stderr: '', stdout: '' } if command.nil? || command.strip.empty?
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                # Clear previous command output.
         
     | 
| 
      
 59 
     | 
    
         
            +
                @stdout_lines = []
         
     | 
| 
      
 60 
     | 
    
         
            +
                @stderr_lines = []
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                # Send the command to the shell.
         
     | 
| 
      
 63 
     | 
    
         
            +
                @stdin.puts command
         
     | 
| 
      
 64 
     | 
    
         
            +
                @stdin.flush
         
     | 
| 
      
 65 
     | 
    
         
            +
                
         
     | 
| 
      
 66 
     | 
    
         
            +
                # For non-interactive commands, immediately send the boundary marker
         
     | 
| 
      
 67 
     | 
    
         
            +
                unless interactive
         
     | 
| 
      
 68 
     | 
    
         
            +
                  @stdin.puts "\n_exitstatus=\"$?\""
         
     | 
| 
      
 69 
     | 
    
         
            +
                  @stdin.puts "echo '#{@boundary}'"
         
     | 
| 
      
 70 
     | 
    
         
            +
                  @stdin.puts 'echo $_exitstatus'
         
     | 
| 
      
 71 
     | 
    
         
            +
                  @stdin.flush
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                done = false
         
     | 
| 
      
 75 
     | 
    
         
            +
                # Add timeout to prevent infinite loops
         
     | 
| 
      
 76 
     | 
    
         
            +
                timeout = Time.now + 10 # 10 second timeout
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                # Loop to concurrently read stdout and stderr.
         
     | 
| 
      
 79 
     | 
    
         
            +
                until done
         
     | 
| 
      
 80 
     | 
    
         
            +
                  # Check if stdin is ready for writing (process is waiting for input)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 82 
     | 
    
         
            +
                    stdin_ready = IO.select(nil, [@stdin], nil, 0)
         
     | 
| 
      
 83 
     | 
    
         
            +
                    @waiting_for_input = interactive && !stdin_ready.nil? && stdin_ready[1].include?(@stdin)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  rescue IOError, Errno::EBADF
         
     | 
| 
      
 85 
     | 
    
         
            +
                    # Handle closed file descriptors
         
     | 
| 
      
 86 
     | 
    
         
            +
                    @waiting_for_input = false
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
      
 88 
     | 
    
         
            +
                  
         
     | 
| 
      
 89 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 90 
     | 
    
         
            +
                    ready = IO.select([@stdout_stream, @stderr_stream], nil, nil, 0.1)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  rescue IOError, Errno::EBADF
         
     | 
| 
      
 92 
     | 
    
         
            +
                    # Handle closed file descriptors
         
     | 
| 
      
 93 
     | 
    
         
            +
                    break
         
     | 
| 
      
 94 
     | 
    
         
            +
                  end
         
     | 
| 
      
 95 
     | 
    
         
            +
                  
         
     | 
| 
      
 96 
     | 
    
         
            +
                  if Time.now > timeout
         
     | 
| 
      
 97 
     | 
    
         
            +
                    @waiting_for_input = false
         
     | 
| 
      
 98 
     | 
    
         
            +
                    # Send boundary marker to finish the command
         
     | 
| 
      
 99 
     | 
    
         
            +
                    @stdin.puts "\n_exitstatus=\"$?\""
         
     | 
| 
      
 100 
     | 
    
         
            +
                    @stdin.puts "echo '#{@boundary}'"
         
     | 
| 
      
 101 
     | 
    
         
            +
                    @stdin.puts 'echo $_exitstatus'
         
     | 
| 
      
 102 
     | 
    
         
            +
                    @stdin.flush
         
     | 
| 
      
 103 
     | 
    
         
            +
                    break
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                  next unless ready # Skip if no IO is ready
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                  ready[0].each do |io|
         
     | 
| 
      
 109 
     | 
    
         
            +
                    if io == @stdout_stream
         
     | 
| 
      
 110 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 111 
     | 
    
         
            +
                        line = @stdout_stream.gets
         
     | 
| 
      
 112 
     | 
    
         
            +
                      rescue IOError
         
     | 
| 
      
 113 
     | 
    
         
            +
                        next
         
     | 
| 
      
 114 
     | 
    
         
            +
                      end
         
     | 
| 
      
 115 
     | 
    
         
            +
                      next if line.nil?
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                      ts = Time.now
         
     | 
| 
      
 118 
     | 
    
         
            +
                      if line.include?(@boundary)
         
     | 
| 
      
 119 
     | 
    
         
            +
                        prefix = line[0...line.index(@boundary)]
         
     | 
| 
      
 120 
     | 
    
         
            +
                        add_stdout_line(timestamp: ts, line: prefix) unless prefix.empty?
         
     | 
| 
      
 121 
     | 
    
         
            +
                        # Read exit status from the next stdout line.
         
     | 
| 
      
 122 
     | 
    
         
            +
                        begin
         
     | 
| 
      
 123 
     | 
    
         
            +
                          status_line = @stdout_stream.gets
         
     | 
| 
      
 124 
     | 
    
         
            +
                          @exitstatus = status_line.strip.to_i if status_line
         
     | 
| 
      
 125 
     | 
    
         
            +
                        rescue IOError
         
     | 
| 
      
 126 
     | 
    
         
            +
                          # Handle closed file descriptors
         
     | 
| 
      
 127 
     | 
    
         
            +
                        end
         
     | 
| 
      
 128 
     | 
    
         
            +
                        done = true
         
     | 
| 
      
 129 
     | 
    
         
            +
                        break
         
     | 
| 
      
 130 
     | 
    
         
            +
                      else
         
     | 
| 
      
 131 
     | 
    
         
            +
                        add_stdout_line(timestamp: ts, line: line)
         
     | 
| 
      
 132 
     | 
    
         
            +
                      end
         
     | 
| 
      
 133 
     | 
    
         
            +
                    elsif io == @stderr_stream
         
     | 
| 
      
 134 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 135 
     | 
    
         
            +
                        line = @stderr_stream.gets
         
     | 
| 
      
 136 
     | 
    
         
            +
                      rescue IOError
         
     | 
| 
      
 137 
     | 
    
         
            +
                        next
         
     | 
| 
      
 138 
     | 
    
         
            +
                      end
         
     | 
| 
      
 139 
     | 
    
         
            +
                      next if line.nil?
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                      ts = Time.now
         
     | 
| 
      
 142 
     | 
    
         
            +
                      add_stderr_line(timestamp: ts, line: line)
         
     | 
| 
      
 143 
     | 
    
         
            +
                    end
         
     | 
| 
      
 144 
     | 
    
         
            +
                  end
         
     | 
| 
      
 145 
     | 
    
         
            +
                end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                # Replace the potentially hanging stderr capture with a non-blocking check
         
     | 
| 
      
 148 
     | 
    
         
            +
                begin
         
     | 
| 
      
 149 
     | 
    
         
            +
                  if (ready = IO.select([@stderr_stream], nil, nil, 0.1))
         
     | 
| 
      
 150 
     | 
    
         
            +
                    ready[0].each do |io|
         
     | 
| 
      
 151 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 152 
     | 
    
         
            +
                        while io.wait_readable(0.1) && (line = io.read_nonblock(4096))
         
     | 
| 
      
 153 
     | 
    
         
            +
                          ts = Time.now
         
     | 
| 
      
 154 
     | 
    
         
            +
                          line.each_line do |l|
         
     | 
| 
      
 155 
     | 
    
         
            +
                            add_stderr_line(timestamp: ts, line: l)
         
     | 
| 
      
 156 
     | 
    
         
            +
                          end
         
     | 
| 
      
 157 
     | 
    
         
            +
                        end
         
     | 
| 
      
 158 
     | 
    
         
            +
                      rescue IO::WaitReadable, IOError, Errno::EBADF
         
     | 
| 
      
 159 
     | 
    
         
            +
                        # No more data available right now or closed file descriptor
         
     | 
| 
      
 160 
     | 
    
         
            +
                      rescue EOFError
         
     | 
| 
      
 161 
     | 
    
         
            +
                        # End of stream reached
         
     | 
| 
      
 162 
     | 
    
         
            +
                      end
         
     | 
| 
      
 163 
     | 
    
         
            +
                    end
         
     | 
| 
      
 164 
     | 
    
         
            +
                  end
         
     | 
| 
      
 165 
     | 
    
         
            +
                rescue IOError, Errno::EBADF
         
     | 
| 
      
 166 
     | 
    
         
            +
                  # Handle closed file descriptors
         
     | 
| 
      
 167 
     | 
    
         
            +
                end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                # Prepare outputs.
         
     | 
| 
      
 170 
     | 
    
         
            +
                @output = @stdout_lines.map { |entry| entry[:line] }.join
         
     | 
| 
      
 171 
     | 
    
         
            +
                @stdout = @output
         
     | 
| 
      
 172 
     | 
    
         
            +
                @stderr = @stderr_lines.map { |entry| entry[:line] }.join
         
     | 
| 
      
 173 
     | 
    
         
            +
                @lines = @stdout_lines.map { |entry| entry[:line] }
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                { status: @exitstatus, output: @output, stdout: @stdout, stderr: @stderr }
         
     | 
| 
      
 176 
     | 
    
         
            +
              end
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
              # Closes the bash session.
         
     | 
| 
      
 179 
     | 
    
         
            +
              def close
         
     | 
| 
      
 180 
     | 
    
         
            +
                @stdin.puts 'exit'
         
     | 
| 
      
 181 
     | 
    
         
            +
                @stdin.flush
         
     | 
| 
      
 182 
     | 
    
         
            +
                @stdin.close
         
     | 
| 
      
 183 
     | 
    
         
            +
                @stdout_stream.close
         
     | 
| 
      
 184 
     | 
    
         
            +
                @stderr_stream.close
         
     | 
| 
      
 185 
     | 
    
         
            +
                @wait_thr.join
         
     | 
| 
      
 186 
     | 
    
         
            +
              end
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
              # Ensures the shell session is closed when the object is garbage collected.
         
     | 
| 
      
 189 
     | 
    
         
            +
              def finalize
         
     | 
| 
      
 190 
     | 
    
         
            +
                close unless @stdin.closed?
         
     | 
| 
      
 191 
     | 
    
         
            +
              end
         
     | 
| 
      
 192 
     | 
    
         
            +
             
     | 
| 
      
 193 
     | 
    
         
            +
              # Sends input to the running process
         
     | 
| 
      
 194 
     | 
    
         
            +
              # @param input [String] The input to send to the process
         
     | 
| 
      
 195 
     | 
    
         
            +
              def send_input(input)
         
     | 
| 
      
 196 
     | 
    
         
            +
                return unless @waiting_for_input
         
     | 
| 
      
 197 
     | 
    
         
            +
                
         
     | 
| 
      
 198 
     | 
    
         
            +
                begin
         
     | 
| 
      
 199 
     | 
    
         
            +
                  @stdin.puts(input)
         
     | 
| 
      
 200 
     | 
    
         
            +
                  @stdin.flush
         
     | 
| 
      
 201 
     | 
    
         
            +
                rescue IOError, Errno::EBADF
         
     | 
| 
      
 202 
     | 
    
         
            +
                  @waiting_for_input = false
         
     | 
| 
      
 203 
     | 
    
         
            +
                  return
         
     | 
| 
      
 204 
     | 
    
         
            +
                end
         
     | 
| 
      
 205 
     | 
    
         
            +
                
         
     | 
| 
      
 206 
     | 
    
         
            +
                # After sending input, we need to check if we're still waiting
         
     | 
| 
      
 207 
     | 
    
         
            +
                # Give a small delay for the process to consume the input
         
     | 
| 
      
 208 
     | 
    
         
            +
                sleep 0.2
         
     | 
| 
      
 209 
     | 
    
         
            +
                
         
     | 
| 
      
 210 
     | 
    
         
            +
                # Check if stdin is still ready for writing
         
     | 
| 
      
 211 
     | 
    
         
            +
                begin
         
     | 
| 
      
 212 
     | 
    
         
            +
                  stdin_ready = IO.select(nil, [@stdin], nil, 0)
         
     | 
| 
      
 213 
     | 
    
         
            +
                  @waiting_for_input = !stdin_ready.nil? && stdin_ready[1].include?(@stdin)
         
     | 
| 
      
 214 
     | 
    
         
            +
                rescue IOError, Errno::EBADF
         
     | 
| 
      
 215 
     | 
    
         
            +
                  @waiting_for_input = false
         
     | 
| 
      
 216 
     | 
    
         
            +
                end
         
     | 
| 
      
 217 
     | 
    
         
            +
                
         
     | 
| 
      
 218 
     | 
    
         
            +
                # If we're no longer waiting for input, send the boundary marker
         
     | 
| 
      
 219 
     | 
    
         
            +
                unless @waiting_for_input
         
     | 
| 
      
 220 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 221 
     | 
    
         
            +
                    @stdin.puts "\n_exitstatus=\"$?\""
         
     | 
| 
      
 222 
     | 
    
         
            +
                    @stdin.puts "echo '#{@boundary}'"
         
     | 
| 
      
 223 
     | 
    
         
            +
                    @stdin.puts 'echo $_exitstatus'
         
     | 
| 
      
 224 
     | 
    
         
            +
                    @stdin.flush
         
     | 
| 
      
 225 
     | 
    
         
            +
                  rescue IOError, Errno::EBADF
         
     | 
| 
      
 226 
     | 
    
         
            +
                    # Handle closed file descriptors
         
     | 
| 
      
 227 
     | 
    
         
            +
                  end
         
     | 
| 
      
 228 
     | 
    
         
            +
                end
         
     | 
| 
      
 229 
     | 
    
         
            +
              end
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
              # Checks if the process is waiting for input
         
     | 
| 
      
 232 
     | 
    
         
            +
              # @return [Boolean] True if the process is waiting for input
         
     | 
| 
      
 233 
     | 
    
         
            +
              def waiting_for_input?
         
     | 
| 
      
 234 
     | 
    
         
            +
                @waiting_for_input
         
     | 
| 
      
 235 
     | 
    
         
            +
              end
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
              private
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
              def add_stdout_line(timestamp:, line:)
         
     | 
| 
      
 240 
     | 
    
         
            +
                @stdout_lines << { timestamp: timestamp, line: line,
         
     | 
| 
      
 241 
     | 
    
         
            +
                                   sequence: @line_counter }
         
     | 
| 
      
 242 
     | 
    
         
            +
                @line_counter += 1
         
     | 
| 
      
 243 
     | 
    
         
            +
              end
         
     | 
| 
      
 244 
     | 
    
         
            +
             
     | 
| 
      
 245 
     | 
    
         
            +
              def add_stderr_line(timestamp:, line:)
         
     | 
| 
      
 246 
     | 
    
         
            +
                @stderr_lines << { timestamp: timestamp, line: line,
         
     | 
| 
      
 247 
     | 
    
         
            +
                                   sequence: @line_counter }
         
     | 
| 
      
 248 
     | 
    
         
            +
                @line_counter += 1
         
     | 
| 
      
 249 
     | 
    
         
            +
              end
         
     | 
| 
      
 250 
     | 
    
         
            +
            end
         
     | 
| 
      
 251 
     | 
    
         
            +
             
     | 
| 
      
 252 
     | 
    
         
            +
            return if $PROGRAM_NAME != __FILE__
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
      
 254 
     | 
    
         
            +
            require 'bundler/setup'
         
     | 
| 
      
 255 
     | 
    
         
            +
            Bundler.require(:default)
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
            require 'minitest/autorun'
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
            class ShellSessionTest < Minitest::Test
         
     | 
| 
      
 260 
     | 
    
         
            +
              def setup
         
     | 
| 
      
 261 
     | 
    
         
            +
                @shell = ShellSession.new
         
     | 
| 
      
 262 
     | 
    
         
            +
              end
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
              def teardown
         
     | 
| 
      
 265 
     | 
    
         
            +
                @shell.close
         
     | 
| 
      
 266 
     | 
    
         
            +
              end
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
              def test_initialize_without_setup_command
         
     | 
| 
      
 269 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 270 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, @shell.exitstatus
         
     | 
| 
      
 271 
     | 
    
         
            +
                  assert_empty @shell.output
         
     | 
| 
      
 272 
     | 
    
         
            +
                  assert_empty @shell.lines
         
     | 
| 
      
 273 
     | 
    
         
            +
                end
         
     | 
| 
      
 274 
     | 
    
         
            +
              end
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
              def test_initialize_with_setup_command
         
     | 
| 
      
 277 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 278 
     | 
    
         
            +
                  shell = ShellSession.new('echo "setup complete"')
         
     | 
| 
      
 279 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, shell.exitstatus
         
     | 
| 
      
 280 
     | 
    
         
            +
                  assert_equal 'setup complete', shell.output.strip
         
     | 
| 
      
 281 
     | 
    
         
            +
                  shell.close
         
     | 
| 
      
 282 
     | 
    
         
            +
                end
         
     | 
| 
      
 283 
     | 
    
         
            +
              end
         
     | 
| 
      
 284 
     | 
    
         
            +
             
     | 
| 
      
 285 
     | 
    
         
            +
              def test_run_command_with_empty_command
         
     | 
| 
      
 286 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 287 
     | 
    
         
            +
                  result = @shell.run_command('')
         
     | 
| 
      
 288 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 289 
     | 
    
         
            +
                  assert_empty result[:output]
         
     | 
| 
      
 290 
     | 
    
         
            +
                end
         
     | 
| 
      
 291 
     | 
    
         
            +
              end
         
     | 
| 
      
 292 
     | 
    
         
            +
             
     | 
| 
      
 293 
     | 
    
         
            +
              def test_run_command_with_nil_command
         
     | 
| 
      
 294 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 295 
     | 
    
         
            +
                  result = @shell.run_command(nil)
         
     | 
| 
      
 296 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 297 
     | 
    
         
            +
                  assert_empty result[:output]
         
     | 
| 
      
 298 
     | 
    
         
            +
                end
         
     | 
| 
      
 299 
     | 
    
         
            +
              end
         
     | 
| 
      
 300 
     | 
    
         
            +
             
     | 
| 
      
 301 
     | 
    
         
            +
              def test_run_command_echo
         
     | 
| 
      
 302 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 303 
     | 
    
         
            +
                  result = @shell.run_command('echo "hello world"')
         
     | 
| 
      
 304 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 305 
     | 
    
         
            +
                  assert_equal 'hello world', result[:output].strip
         
     | 
| 
      
 306 
     | 
    
         
            +
                end
         
     | 
| 
      
 307 
     | 
    
         
            +
              end
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
              def test_run_command_with_multiple_lines
         
     | 
| 
      
 310 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 311 
     | 
    
         
            +
                  result = @shell.run_command("echo 'line1'\necho 'line2'")
         
     | 
| 
      
 312 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 313 
     | 
    
         
            +
                  assert_equal "line1\nline2\n", result[:output]
         
     | 
| 
      
 314 
     | 
    
         
            +
                  assert_equal %w[line1 line2],
         
     | 
| 
      
 315 
     | 
    
         
            +
                               @shell.lines.map(&:strip).reject(&:empty?)
         
     | 
| 
      
 316 
     | 
    
         
            +
                end
         
     | 
| 
      
 317 
     | 
    
         
            +
              end
         
     | 
| 
      
 318 
     | 
    
         
            +
             
     | 
| 
      
 319 
     | 
    
         
            +
              def test_run_command_with_error
         
     | 
| 
      
 320 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 321 
     | 
    
         
            +
                  result = @shell.run_command('nonexistent_command')
         
     | 
| 
      
 322 
     | 
    
         
            +
                  refute_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 323 
     | 
    
         
            +
                end
         
     | 
| 
      
 324 
     | 
    
         
            +
              end
         
     | 
| 
      
 325 
     | 
    
         
            +
             
     | 
| 
      
 326 
     | 
    
         
            +
              def test_run_multiple_commands_in_sequence
         
     | 
| 
      
 327 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 328 
     | 
    
         
            +
                  @shell.run_command('echo "first"')
         
     | 
| 
      
 329 
     | 
    
         
            +
                  result = @shell.run_command('echo "second"')
         
     | 
| 
      
 330 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 331 
     | 
    
         
            +
                  assert_equal 'second', result[:output].strip
         
     | 
| 
      
 332 
     | 
    
         
            +
                end
         
     | 
| 
      
 333 
     | 
    
         
            +
              end
         
     | 
| 
      
 334 
     | 
    
         
            +
             
     | 
| 
      
 335 
     | 
    
         
            +
              def test_close_and_reopen
         
     | 
| 
      
 336 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 337 
     | 
    
         
            +
                  @shell.close
         
     | 
| 
      
 338 
     | 
    
         
            +
                  assert @shell.instance_variable_get(:@stdin).closed?
         
     | 
| 
      
 339 
     | 
    
         
            +
             
     | 
| 
      
 340 
     | 
    
         
            +
                  # Create new session.
         
     | 
| 
      
 341 
     | 
    
         
            +
                  @shell = ShellSession.new
         
     | 
| 
      
 342 
     | 
    
         
            +
                  result = @shell.run_command('echo "test"')
         
     | 
| 
      
 343 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 344 
     | 
    
         
            +
                end
         
     | 
| 
      
 345 
     | 
    
         
            +
              end
         
     | 
| 
      
 346 
     | 
    
         
            +
             
     | 
| 
      
 347 
     | 
    
         
            +
              def test_stdout_and_stderr_separation
         
     | 
| 
      
 348 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 349 
     | 
    
         
            +
                  result = @shell.run_command('echo "to stdout" && echo "to stderr" >&2')
         
     | 
| 
      
 350 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 351 
     | 
    
         
            +
                  assert_equal 'to stdout', result[:stdout].strip
         
     | 
| 
      
 352 
     | 
    
         
            +
                  assert_equal 'to stderr', result[:stderr].strip
         
     | 
| 
      
 353 
     | 
    
         
            +
                end
         
     | 
| 
      
 354 
     | 
    
         
            +
              end
         
     | 
| 
      
 355 
     | 
    
         
            +
             
     | 
| 
      
 356 
     | 
    
         
            +
              def test_stdout_and_stderr_timestamps
         
     | 
| 
      
 357 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 358 
     | 
    
         
            +
                  @shell.run_command('echo "stdout line" && echo "stderr line" >&2')
         
     | 
| 
      
 359 
     | 
    
         
            +
             
     | 
| 
      
 360 
     | 
    
         
            +
                  stdout_entry = @shell.stdout_lines.first
         
     | 
| 
      
 361 
     | 
    
         
            +
                  stderr_entry = @shell.stderr_lines.first
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
                  assert_instance_of Time, stdout_entry[:timestamp]
         
     | 
| 
      
 364 
     | 
    
         
            +
                  assert_instance_of Time, stderr_entry[:timestamp]
         
     | 
| 
      
 365 
     | 
    
         
            +
                  assert_equal "stdout line\n", stdout_entry[:line]
         
     | 
| 
      
 366 
     | 
    
         
            +
                  assert_equal "stderr line\n", stderr_entry[:line]
         
     | 
| 
      
 367 
     | 
    
         
            +
                  assert_equal 0, stdout_entry[:sequence]
         
     | 
| 
      
 368 
     | 
    
         
            +
                  assert_equal 1, stderr_entry[:sequence]
         
     | 
| 
      
 369 
     | 
    
         
            +
                end
         
     | 
| 
      
 370 
     | 
    
         
            +
              end
         
     | 
| 
      
 371 
     | 
    
         
            +
             
     | 
| 
      
 372 
     | 
    
         
            +
              def test_stderr_only_output
         
     | 
| 
      
 373 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 374 
     | 
    
         
            +
                  result = @shell.run_command('echo "error message" >&2')
         
     | 
| 
      
 375 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 376 
     | 
    
         
            +
                  assert_empty result[:stdout]
         
     | 
| 
      
 377 
     | 
    
         
            +
                  assert_equal 'error message', result[:stderr].strip
         
     | 
| 
      
 378 
     | 
    
         
            +
                end
         
     | 
| 
      
 379 
     | 
    
         
            +
              end
         
     | 
| 
      
 380 
     | 
    
         
            +
             
     | 
| 
      
 381 
     | 
    
         
            +
              def test_interleaved_stdout_stderr
         
     | 
| 
      
 382 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 383 
     | 
    
         
            +
                  cmd = <<~SHELL
         
     | 
| 
      
 384 
     | 
    
         
            +
                    echo "out1"
         
     | 
| 
      
 385 
     | 
    
         
            +
                    sleep 0.1
         
     | 
| 
      
 386 
     | 
    
         
            +
                    echo "err1" >&2
         
     | 
| 
      
 387 
     | 
    
         
            +
                    sleep 0.1
         
     | 
| 
      
 388 
     | 
    
         
            +
                    echo "out2"
         
     | 
| 
      
 389 
     | 
    
         
            +
                    sleep 0.1
         
     | 
| 
      
 390 
     | 
    
         
            +
                    echo "err2" >&2
         
     | 
| 
      
 391 
     | 
    
         
            +
                  SHELL
         
     | 
| 
      
 392 
     | 
    
         
            +
             
     | 
| 
      
 393 
     | 
    
         
            +
                  result = @shell.run_command(cmd)
         
     | 
| 
      
 394 
     | 
    
         
            +
                  assert_equal STATUS_SUCCESS, result[:status]
         
     | 
| 
      
 395 
     | 
    
         
            +
                  assert_equal "out1\nout2\n", result[:stdout]
         
     | 
| 
      
 396 
     | 
    
         
            +
                  assert_equal "err1\nerr2\n", result[:stderr]
         
     | 
| 
      
 397 
     | 
    
         
            +
             
     | 
| 
      
 398 
     | 
    
         
            +
                  # Verify sequence numbers
         
     | 
| 
      
 399 
     | 
    
         
            +
                  all_lines = (@shell.stdout_lines + @shell.stderr_lines).sort_by do |entry|
         
     | 
| 
      
 400 
     | 
    
         
            +
                    entry[:sequence]
         
     | 
| 
      
 401 
     | 
    
         
            +
                  end
         
     | 
| 
      
 402 
     | 
    
         
            +
                  assert_equal(%W[out1\n err1\n out2\n err2\n], all_lines.map do |entry|
         
     | 
| 
      
 403 
     | 
    
         
            +
                                                                  entry[:line]
         
     | 
| 
      
 404 
     | 
    
         
            +
                                                                end)
         
     | 
| 
      
 405 
     | 
    
         
            +
                  assert_equal([0, 1, 2, 3], all_lines.map { |entry| entry[:sequence] })
         
     | 
| 
      
 406 
     | 
    
         
            +
                end
         
     | 
| 
      
 407 
     | 
    
         
            +
              end
         
     | 
| 
      
 408 
     | 
    
         
            +
             
     | 
| 
      
 409 
     | 
    
         
            +
              def test_detect_waiting_for_input
         
     | 
| 
      
 410 
     | 
    
         
            +
                # Skip this test for now until we can fix the interactive command handling
         
     | 
| 
      
 411 
     | 
    
         
            +
                skip "Skipping due to issues with interactive command handling"
         
     | 
| 
      
 412 
     | 
    
         
            +
              end
         
     | 
| 
      
 413 
     | 
    
         
            +
              
         
     | 
| 
      
 414 
     | 
    
         
            +
              def test_send_input_when_not_waiting_does_nothing
         
     | 
| 
      
 415 
     | 
    
         
            +
                Timeout.timeout(5) do
         
     | 
| 
      
 416 
     | 
    
         
            +
                  # Run a command that doesn't wait for input
         
     | 
| 
      
 417 
     | 
    
         
            +
                  @shell.run_command('echo "No input needed"')
         
     | 
| 
      
 418 
     | 
    
         
            +
                  
         
     | 
| 
      
 419 
     | 
    
         
            +
                  # Verify we're not waiting for input
         
     | 
| 
      
 420 
     | 
    
         
            +
                  refute @shell.waiting_for_input?, "Should not be waiting for input"
         
     | 
| 
      
 421 
     | 
    
         
            +
                  
         
     | 
| 
      
 422 
     | 
    
         
            +
                  # Sending input should have no effect
         
     | 
| 
      
 423 
     | 
    
         
            +
                  @shell.send_input("ignored input")
         
     | 
| 
      
 424 
     | 
    
         
            +
                  
         
     | 
| 
      
 425 
     | 
    
         
            +
                  # Output should be unchanged
         
     | 
| 
      
 426 
     | 
    
         
            +
                  assert_equal "No input needed\n", @shell.output
         
     | 
| 
      
 427 
     | 
    
         
            +
                end
         
     | 
| 
      
 428 
     | 
    
         
            +
              end
         
     | 
| 
      
 429 
     | 
    
         
            +
              
         
     | 
| 
      
 430 
     | 
    
         
            +
              def test_waiting_for_input_predicate_method
         
     | 
| 
      
 431 
     | 
    
         
            +
                # Skip this test for now until we can fix the interactive command handling
         
     | 
| 
      
 432 
     | 
    
         
            +
                skip "Skipping due to issues with interactive command handling"
         
     | 
| 
      
 433 
     | 
    
         
            +
              end
         
     | 
| 
      
 434 
     | 
    
         
            +
              
         
     | 
| 
      
 435 
     | 
    
         
            +
              def test_multiple_input_prompts
         
     | 
| 
      
 436 
     | 
    
         
            +
                # Skip this test for now until we can fix the interactive command handling
         
     | 
| 
      
 437 
     | 
    
         
            +
                skip "Skipping due to issues with interactive command handling"
         
     | 
| 
      
 438 
     | 
    
         
            +
              end
         
     | 
| 
      
 439 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/streams_out.rb
    CHANGED
    
    
    
        data/lib/string_util.rb
    CHANGED
    
    | 
         @@ -1,4 +1,3 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            #!/usr/bin/env ruby
         
     | 
| 
       2 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       3 
2 
     | 
    
         | 
| 
       4 
3 
     | 
    
         
             
            # encoding=utf-8
         
     | 
| 
         @@ -19,3 +18,14 @@ module StringUtil 
     | 
|
| 
       19 
18 
     | 
    
         
             
                end
         
     | 
| 
       20 
19 
     | 
    
         
             
              end
         
     | 
| 
       21 
20 
     | 
    
         
             
            end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            class String
         
     | 
| 
      
 23 
     | 
    
         
            +
              unless method_defined?(:present?)
         
     | 
| 
      
 24 
     | 
    
         
            +
                # Checks if the string contains any non-whitespace characters.
         
     | 
| 
      
 25 
     | 
    
         
            +
                # @return [Boolean] Returns true if the string contains non-whitespace
         
     | 
| 
      
 26 
     | 
    
         
            +
                # characters, false otherwise.
         
     | 
| 
      
 27 
     | 
    
         
            +
                def present?
         
     | 
| 
      
 28 
     | 
    
         
            +
                  !strip.empty?
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,112 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env -S bundle exec ruby
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            # encoding=utf-8
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            require 'singleton'
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            ##
         
     | 
| 
      
 11 
     | 
    
         
            +
            # SuccessResult represents a successful outcome when no specific result value is produced.
         
     | 
| 
      
 12 
     | 
    
         
            +
            #
         
     | 
| 
      
 13 
     | 
    
         
            +
            # This class follows the Null Object pattern for successful cases, ensuring a consistent
         
     | 
| 
      
 14 
     | 
    
         
            +
            # interface with methods such as #success? and #failure?. It is implemented as a singleton,
         
     | 
| 
      
 15 
     | 
    
         
            +
            # meaning there is only one instance of SuccessResult available.
         
     | 
| 
      
 16 
     | 
    
         
            +
            #
         
     | 
| 
      
 17 
     | 
    
         
            +
            # Example:
         
     | 
| 
      
 18 
     | 
    
         
            +
            #   result = SomeService.call
         
     | 
| 
      
 19 
     | 
    
         
            +
            #   if result.success?
         
     | 
| 
      
 20 
     | 
    
         
            +
            #     # proceed knowing the operation succeeded
         
     | 
| 
      
 21 
     | 
    
         
            +
            #   else
         
     | 
| 
      
 22 
     | 
    
         
            +
            #     # handle failure
         
     | 
| 
      
 23 
     | 
    
         
            +
            #   end
         
     | 
| 
      
 24 
     | 
    
         
            +
            #
         
     | 
| 
      
 25 
     | 
    
         
            +
            class SuccessResult
         
     | 
| 
      
 26 
     | 
    
         
            +
              include Singleton
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              ##
         
     | 
| 
      
 29 
     | 
    
         
            +
              # Indicates that the result is a success.
         
     | 
| 
      
 30 
     | 
    
         
            +
              #
         
     | 
| 
      
 31 
     | 
    
         
            +
              # @return [Boolean] always true for SuccessResult
         
     | 
| 
      
 32 
     | 
    
         
            +
              def success?
         
     | 
| 
      
 33 
     | 
    
         
            +
                true
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              ##
         
     | 
| 
      
 37 
     | 
    
         
            +
              # Indicates that the result is not a failure.
         
     | 
| 
      
 38 
     | 
    
         
            +
              #
         
     | 
| 
      
 39 
     | 
    
         
            +
              # @return [Boolean] always false for SuccessResult
         
     | 
| 
      
 40 
     | 
    
         
            +
              def failure?
         
     | 
| 
      
 41 
     | 
    
         
            +
                false
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              ##
         
     | 
| 
      
 45 
     | 
    
         
            +
              # Provides a default message for the successful result.
         
     | 
| 
      
 46 
     | 
    
         
            +
              #
         
     | 
| 
      
 47 
     | 
    
         
            +
              # @return [String] a message indicating success
         
     | 
| 
      
 48 
     | 
    
         
            +
              def message
         
     | 
| 
      
 49 
     | 
    
         
            +
                'Success'
         
     | 
| 
      
 50 
     | 
    
         
            +
              end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              ##
         
     | 
| 
      
 53 
     | 
    
         
            +
              # Returns a string representation of this SuccessResult.
         
     | 
| 
      
 54 
     | 
    
         
            +
              #
         
     | 
| 
      
 55 
     | 
    
         
            +
              # @return [String]
         
     | 
| 
      
 56 
     | 
    
         
            +
              def to_s
         
     | 
| 
      
 57 
     | 
    
         
            +
                'SuccessResult'
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
            end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
            # Default instance for ease-of-use.
         
     | 
| 
      
 62 
     | 
    
         
            +
            DEFAULT_SUCCESS_RESULT = SuccessResult.instance
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
            return unless $PROGRAM_NAME == __FILE__
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            require 'bundler/setup'
         
     | 
| 
      
 67 
     | 
    
         
            +
            Bundler.require(:default)
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
            require 'minitest/autorun'
         
     | 
| 
      
 70 
     | 
    
         
            +
            require 'mocha/minitest'
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
            require_relative 'ww'
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
            ##
         
     | 
| 
      
 75 
     | 
    
         
            +
            # Tests for the SuccessResult class.
         
     | 
| 
      
 76 
     | 
    
         
            +
            #
         
     | 
| 
      
 77 
     | 
    
         
            +
            # This suite verifies that the SuccessResult singleton behaves as expected:
         
     | 
| 
      
 78 
     | 
    
         
            +
            # - It is a singleton (all calls to SuccessResult.instance return the same object)
         
     | 
| 
      
 79 
     | 
    
         
            +
            # - The #success? method returns true and #failure? returns false
         
     | 
| 
      
 80 
     | 
    
         
            +
            # - The default message and string representation are correct.
         
     | 
| 
      
 81 
     | 
    
         
            +
            #
         
     | 
| 
      
 82 
     | 
    
         
            +
            class SuccessResultTest < Minitest::Test
         
     | 
| 
      
 83 
     | 
    
         
            +
              def test_singleton
         
     | 
| 
      
 84 
     | 
    
         
            +
                instance1 = SuccessResult.instance
         
     | 
| 
      
 85 
     | 
    
         
            +
                instance2 = SuccessResult.instance
         
     | 
| 
      
 86 
     | 
    
         
            +
                assert_same instance1, instance2, "Expected the singleton instances to be identical"
         
     | 
| 
      
 87 
     | 
    
         
            +
              end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
              def test_success_method
         
     | 
| 
      
 90 
     | 
    
         
            +
                sr = SuccessResult.instance
         
     | 
| 
      
 91 
     | 
    
         
            +
                assert sr.success?, "Expected success? to return true"
         
     | 
| 
      
 92 
     | 
    
         
            +
              end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
              def test_failure_method
         
     | 
| 
      
 95 
     | 
    
         
            +
                sr = SuccessResult.instance
         
     | 
| 
      
 96 
     | 
    
         
            +
                refute sr.failure?, "Expected failure? to return false"
         
     | 
| 
      
 97 
     | 
    
         
            +
              end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
              def test_message
         
     | 
| 
      
 100 
     | 
    
         
            +
                sr = SuccessResult.instance
         
     | 
| 
      
 101 
     | 
    
         
            +
                assert_equal 'Success', sr.message, "Expected message to be 'Success'"
         
     | 
| 
      
 102 
     | 
    
         
            +
              end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
              def test_to_s
         
     | 
| 
      
 105 
     | 
    
         
            +
                sr = SuccessResult.instance
         
     | 
| 
      
 106 
     | 
    
         
            +
                assert_equal 'SuccessResult', sr.to_s, "Expected to_s to return 'SuccessResult'"
         
     | 
| 
      
 107 
     | 
    
         
            +
              end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
              def test_default_success_result_constant
         
     | 
| 
      
 110 
     | 
    
         
            +
                assert_same SuccessResult.instance, DEFAULT_SUCCESS_RESULT, "Expected DEFAULT_SUCCESS_RESULT to be the same singleton instance"
         
     | 
| 
      
 111 
     | 
    
         
            +
              end
         
     | 
| 
      
 112 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/text_analyzer.rb
    CHANGED
    
    
    
        data/lib/ww.rb
    CHANGED
    
    | 
         @@ -1,10 +1,11 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            # encoding=utf-8
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'bundler/setup' # Bundler enforces gem versions
         
     | 
| 
       4 
5 
     | 
    
         
             
            require 'pp'
         
     | 
| 
       5 
6 
     | 
    
         
             
            require 'stringio'
         
     | 
| 
       6 
7 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
            LOG_LEVELS = %i[debug info warn error fatal]
         
     | 
| 
      
 8 
     | 
    
         
            +
            LOG_LEVELS = %i[debug info warn error fatal].freeze
         
     | 
| 
       8 
9 
     | 
    
         | 
| 
       9 
10 
     | 
    
         
             
            $debug = $DEBUG || !ENV['WW'].nil?
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
         @@ -15,7 +16,8 @@ if $debug && ENV['WW_MINIMUM'].nil? 
     | 
|
| 
       15 
16 
     | 
    
         
             
            end
         
     | 
| 
       16 
17 
     | 
    
         | 
| 
       17 
18 
     | 
    
         
             
            def ww(*objs, **kwargs)
         
     | 
| 
       18 
     | 
    
         
            -
              return  
     | 
| 
      
 19 
     | 
    
         
            +
              # return the last item in the list, as the label is usually first
         
     | 
| 
      
 20 
     | 
    
         
            +
              return objs.last unless $debug
         
     | 
| 
       19 
21 
     | 
    
         | 
| 
       20 
22 
     | 
    
         
             
              ww0(*objs, **kwargs.merge(locations: caller_locations))
         
     | 
| 
       21 
23 
     | 
    
         
             
            end
         
     | 
| 
         @@ -55,12 +57,11 @@ def ww0(*objs, 
     | 
|
| 
       55 
57 
     | 
    
         
             
              # Combine all parts into the final message
         
     | 
| 
       56 
58 
     | 
    
         
             
              header = "#{time_prefix}#{level_prefix} #{category_prefix}"
         
     | 
| 
       57 
59 
     | 
    
         
             
              trace = backtrace + objs
         
     | 
| 
      
 60 
     | 
    
         
            +
              io = StringIO.new
         
     | 
| 
       58 
61 
     | 
    
         
             
              formatted_message = if single_line
         
     | 
| 
       59 
     | 
    
         
            -
                                    io = StringIO.new
         
     | 
| 
       60 
62 
     | 
    
         
             
                                    PP.singleline_pp(trace, io)
         
     | 
| 
       61 
63 
     | 
    
         
             
                                    "#{header} #{io.string}"
         
     | 
| 
       62 
64 
     | 
    
         
             
                                  else
         
     | 
| 
       63 
     | 
    
         
            -
                                    io = StringIO.new
         
     | 
| 
       64 
65 
     | 
    
         
             
                                    PP.pp(trace, io)
         
     | 
| 
       65 
66 
     | 
    
         
             
                                    "#{header}\n#{io.string}"
         
     | 
| 
       66 
67 
     | 
    
         
             
                                  end
         
     | 
| 
         @@ -76,14 +77,15 @@ def ww0(*objs, 
     | 
|
| 
       76 
77 
     | 
    
         
             
                file.puts(formatted_message)
         
     | 
| 
       77 
78 
     | 
    
         
             
              end
         
     | 
| 
       78 
79 
     | 
    
         | 
| 
       79 
     | 
    
         
            -
               
     | 
| 
      
 80 
     | 
    
         
            +
              # return the last item in the list, as the label is usually first
         
     | 
| 
      
 81 
     | 
    
         
            +
              objs.last
         
     | 
| 
       80 
82 
     | 
    
         
             
            end
         
     | 
| 
       81 
83 
     | 
    
         | 
| 
       82 
84 
     | 
    
         
             
            class Array
         
     | 
| 
       83 
85 
     | 
    
         
             
              unless defined?(deref)
         
     | 
| 
       84 
86 
     | 
    
         
             
                def deref
         
     | 
| 
       85 
     | 
    
         
            -
                  map(&:deref). 
     | 
| 
       86 
     | 
    
         
            -
                     
     | 
| 
      
 87 
     | 
    
         
            +
                  map(&:deref).reject do |line|
         
     | 
| 
      
 88 
     | 
    
         
            +
                    %r{^/(vendor|\.bundle)/}.match(line)
         
     | 
| 
       87 
89 
     | 
    
         
             
                  end
         
     | 
| 
       88 
90 
     | 
    
         
             
                end
         
     | 
| 
       89 
91 
     | 
    
         
             
              end
         
     |