From 32fff6e1d1df9754a728dc2b6695b559c1280b83 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 3 May 2024 07:30:49 +1200 Subject: [PATCH] Separation of output formatting from log generation/schema. (#60) --- .github/workflows/documentation.yaml | 4 +- .github/workflows/test-external.yaml | 1 - .github/workflows/test.yaml | 1 - config/external.yaml | 6 + console.gemspec | 8 +- fixtures/console/captured_output.rb | 20 +++ fixtures/my_module.rb | 42 ------ gems.rb | 8 +- lib/console.rb | 10 +- lib/console/adapter.rb | 3 +- lib/console/buffer.rb | 25 ---- lib/console/capture.rb | 8 +- lib/console/compatible/logger.rb | 3 +- lib/console/event.rb | 3 +- lib/console/event/failure.rb | 87 ++++++------ lib/console/event/generic.rb | 15 +-- lib/console/event/progress.rb | 60 --------- lib/console/event/spawn.rb | 55 ++++---- lib/console/filter.rb | 6 +- lib/console/format.rb | 6 +- lib/console/logger.rb | 28 ++-- lib/console/output.rb | 7 +- lib/console/output/default.rb | 10 +- lib/console/output/json.rb | 16 --- lib/console/output/null.rb | 2 +- lib/console/output/sensitive.rb | 20 +-- .../logger.rb => output/serialized.rb} | 54 +------- lib/console/output/split.rb | 6 +- .../logger.rb => output/terminal.rb} | 126 +++++++++++------- lib/console/output/text.rb | 16 --- lib/console/output/wrapper.rb | 22 +++ lib/console/output/xterm.rb | 16 --- lib/console/progress.rb | 26 ++-- lib/console/split.rb | 10 -- lib/console/terminal.rb | 21 ++- lib/console/terminal/formatter/failure.rb | 57 ++++++++ lib/console/terminal/formatter/progress.rb | 58 ++++++++ lib/console/terminal/formatter/spawn.rb | 42 ++++++ lib/console/terminal/text.rb | 6 +- lib/console/terminal/xterm.rb | 9 +- lib/console/version.rb | 2 +- test.rb | 17 +++ test/console.rb | 112 ++++++++++++---- test/console/compatible/logger.rb | 6 +- test/console/event/failure.rb | 68 ++++------ test/console/{ => event}/progress.rb | 28 ++-- test/console/logger.rb | 14 +- test/console/output.rb | 14 +- test/console/output/default.rb | 12 +- .../logger.rb => output/serialized.rb} | 27 ++-- .../logger.rb => output/terminal.rb} | 6 +- test/console/terminal/formatter/failure.rb | 28 ++++ test/console/terminal/formatter/progress.rb | 27 ++++ 53 files changed, 702 insertions(+), 582 deletions(-) create mode 100644 fixtures/console/captured_output.rb delete mode 100644 fixtures/my_module.rb delete mode 100644 lib/console/buffer.rb delete mode 100644 lib/console/event/progress.rb delete mode 100644 lib/console/output/json.rb rename lib/console/{serialized/logger.rb => output/serialized.rb} (54%) rename lib/console/{terminal/logger.rb => output/terminal.rb} (65%) delete mode 100644 lib/console/output/text.rb create mode 100644 lib/console/output/wrapper.rb delete mode 100644 lib/console/output/xterm.rb delete mode 100644 lib/console/split.rb create mode 100644 lib/console/terminal/formatter/failure.rb create mode 100644 lib/console/terminal/formatter/progress.rb create mode 100644 lib/console/terminal/formatter/spawn.rb create mode 100755 test.rb rename test/console/{ => event}/progress.rb (71%) rename test/console/{serialized/logger.rb => output/serialized.rb} (73%) rename test/console/{terminal/logger.rb => output/terminal.rb} (90%) create mode 100644 test/console/terminal/formatter/failure.rb create mode 100644 test/console/terminal/formatter/progress.rb diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 8dc5227..f5f553a 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -40,7 +40,7 @@ jobs: run: bundle exec bake utopia:project:static --force no - name: Upload documentation artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: docs @@ -55,4 +55,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v3 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test-external.yaml b/.github/workflows/test-external.yaml index 18efa2c..21898f5 100644 --- a/.github/workflows/test-external.yaml +++ b/.github/workflows/test-external.yaml @@ -20,7 +20,6 @@ jobs: - macos ruby: - - "3.0" - "3.1" - "3.2" - "3.3" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1dca864..0769a98 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,7 +21,6 @@ jobs: - macos ruby: - - "3.0" - "3.1" - "3.2" - "3.3" diff --git a/config/external.yaml b/config/external.yaml index 50fd4db..b1faec6 100644 --- a/config/external.yaml +++ b/config/external.yaml @@ -1,3 +1,9 @@ +async: + url: https://github.com/socketry/async + command: bundle exec bake test async-dns: url: https://github.com/socketry/async-dns command: CONSOLE_LEVEL=debug bundle exec rspec +falcon: + url: https://github.com/socketry/falcon + command: CONSOLE_LEVEL=debug bundle exec sus diff --git a/console.gemspec b/console.gemspec index 78cb1dd..2c76c83 100644 --- a/console.gemspec +++ b/console.gemspec @@ -13,11 +13,15 @@ Gem::Specification.new do |spec| spec.cert_chain = ['release.cert'] spec.signing_key = File.expand_path('~/.gem/release.pem') - spec.homepage = "https://socketry.github.io/console/" + spec.homepage = "https://socketry.github.io/console" + + spec.metadata = { + "documentation_uri" => "https://socketry.github.io/console/", + } spec.files = Dir.glob(['{bake,lib}/**/*', '*.md'], File::FNM_DOTMATCH, base: __dir__) - spec.required_ruby_version = ">= 3.0" + spec.required_ruby_version = ">= 3.1" spec.add_dependency "fiber-annotation" spec.add_dependency "fiber-storage" diff --git a/fixtures/console/captured_output.rb b/fixtures/console/captured_output.rb new file mode 100644 index 0000000..f22a4ba --- /dev/null +++ b/fixtures/console/captured_output.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require 'sus/shared' + +module Console + CapturedOutput = Sus::Shared("captured output") do + let(:capture) {Console::Capture.new} + let(:logger) {Console::Logger.new(capture)} + + def around + Fiber.new do + Console.logger = logger + super + end.resume + end + end +end diff --git a/fixtures/my_module.rb b/fixtures/my_module.rb deleted file mode 100644 index d7170f5..0000000 --- a/fixtures/my_module.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. - -module MyModule - extend Console - - def self.argument_error - raise ArgumentError, "It broken!" - end - - def self.nested_error - argument_error - rescue - raise RuntimeError, "Magic smoke escaped!" - end - - def self.log_error - self.nested_error - rescue - logger.error(self, $!) - end - - def self.test_logger - logger.debug "1: GOTO LINE 2", "2: GOTO LINE 1" - - logger.info "Dear maintainer:" do |buffer| - buffer.puts "Once you are done trying to 'optimize' this routine, and have realized what a terrible mistake that was, please increment the following counter as a warning to the next guy:" - buffer.puts "total_hours_wasted_here = 42" - end - - logger.warn "Something didn't work as expected!" - logger.error "There be the dragons!", (raise RuntimeError, "Bits have been rotated incorrectly!" rescue $!) - - logger.info(self) {Console::Shell.for({LDFLAGS: "-lm"}, "gcc", "-o", "stuff.o", "stuff.c", chdir: "/tmp/compile")} - - logger.info(Object.new) {"Where would we be without Object.new?"} - end - - # test_logger -end diff --git a/gems.rb b/gems.rb index 21fc5f5..937d201 100644 --- a/gems.rb +++ b/gems.rb @@ -1,24 +1,22 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. source "https://rubygems.org" -# Specify your gem's dependencies in console.gemspec gemspec group :maintenance, optional: true do gem "bake-gem" gem "bake-modernize" - gem "bake-github-pages" gem "utopia-project" end group :test do - gem "covered", "~> 0.18.1" - gem "sus", "~> 0.14" + gem "sus" + gem "covered" gem "bake-test" gem "bake-test-external" diff --git a/lib/console.rb b/lib/console.rb index a3f83dd..7397722 100644 --- a/lib/console.rb +++ b/lib/console.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2019, by Bryan Powell. # Copyright, 2020, by Michael Adams. # Copyright, 2021, by Cédric Boutillier. @@ -43,12 +43,4 @@ def call(...) Logger.instance.call(...) end end - - def logger= logger - warn "Setting logger on #{self} is deprecated. Use Console.logger= instead.", uplevel: 1 - end - - def logger - Logger.instance - end end diff --git a/lib/console/adapter.rb b/lib/console/adapter.rb index 92fe3a8..b483ae1 100644 --- a/lib/console/adapter.rb +++ b/lib/console/adapter.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2023, by Samuel Williams. +# Copyright, 2023-2024, by Samuel Williams. module Console + # This namespace is reserved for logging adapters provided by other gems. module Adapter end end diff --git a/lib/console/buffer.rb b/lib/console/buffer.rb deleted file mode 100644 index b68975a..0000000 --- a/lib/console/buffer.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. - -require 'stringio' - -module Console - class Buffer < StringIO - def initialize(prefix = nil) - @prefix = prefix - - super() - end - - def puts(*args, prefix: @prefix) - args.each do |arg| - self.write(prefix) if prefix - super(arg) - end - end - - alias << puts - end -end diff --git a/lib/console/capture.rb b/lib/console/capture.rb index 97d71c4..fabeb55 100644 --- a/lib/console/capture.rb +++ b/lib/console/capture.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require_relative 'filter' @@ -40,7 +40,7 @@ def verbose? @verbose end - def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block) + def call(subject = nil, *arguments, severity: UNKNOWN, event: nil, **options, &block) message = { time: ::Time.now.iso8601, severity: severity, @@ -51,6 +51,10 @@ def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block) message[:subject] = subject end + if event + message[:event] = event.to_hash + end + if arguments.any? message[:arguments] = arguments end diff --git a/lib/console/compatible/logger.rb b/lib/console/compatible/logger.rb index ef52fd7..2a4b5d5 100644 --- a/lib/console/compatible/logger.rb +++ b/lib/console/compatible/logger.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2022-2023, by Samuel Williams. +# Copyright, 2022-2024, by Samuel Williams. require 'logger' module Console module Compatible + # A compatible interface for {::Logger} which can be used with {Console}. class Logger < ::Logger class LogDevice def initialize(subject, output) diff --git a/lib/console/event.rb b/lib/console/event.rb index 8673e67..e5cc113 100644 --- a/lib/console/event.rb +++ b/lib/console/event.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require_relative 'event/spawn' require_relative 'event/failure' -require_relative 'event/progress' diff --git a/lib/console/event/failure.rb b/lib/console/event/failure.rb index 2243df3..eb3d80e 100644 --- a/lib/console/event/failure.rb +++ b/lib/console/event/failure.rb @@ -1,83 +1,74 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2021, by Robert Schulze. require_relative 'generic' module Console module Event + # Represents a failure event. + # + # ```ruby + # Console::Event::Failure.for(exception).emit(self) + # ``` class Failure < Generic - def self.current_working_directory + def self.default_root Dir.getwd rescue # e.g. Errno::EMFILE nil end def self.for(exception) - self.new(exception, self.current_working_directory) + self.new(exception, self.default_root) end - def initialize(exception, root = nil) + def self.log(subject, exception, **options) + Console.error(subject, **self.for(exception).to_hash, **options) + end + + def initialize(exception, root = Dir.getwd) @exception = exception @root = root end - attr :exception - attr :root - - def self.register(terminal) - terminal[:exception_title] ||= terminal.style(:red, nil, :bold) - terminal[:exception_detail] ||= terminal.style(:yellow) - terminal[:exception_backtrace] ||= terminal.style(:red) - terminal[:exception_backtrace_other] ||= terminal.style(:red, nil, :faint) - terminal[:exception_message] ||= terminal.style(:default) + def to_hash + Hash.new.tap do |hash| + hash[:type] = :failure + hash[:root] = @root if @root + extract(@exception, hash) + end end - def to_h - {exception: @exception, root: @root} + def emit(*arguments, **options) + options[:severity] ||= :error + + super end - def format(output, terminal, verbose) - format_exception(@exception, nil, output, terminal, verbose) - end + private - if Exception.method_defined?(:detailed_message) - def detailed_message(exception) - exception.detailed_message - end - else - def detailed_message(exception) - exception.message - end - end - - def format_exception(exception, prefix, output, terminal, verbose) - lines = detailed_message(exception).lines.map(&:chomp) - - output.puts " #{prefix}#{terminal[:exception_title]}#{exception.class}#{terminal.reset}: #{lines.shift}" - - lines.each do |line| - output.puts " #{terminal[:exception_detail]}#{line}#{terminal.reset}" - end + def extract(exception, hash) + hash[:class] = exception.class.name - root_pattern = /^#{@root}\// if @root - - exception.backtrace&.each_with_index do |line, index| - path, offset, message = line.split(":", 3) - style = :exception_backtrace + if exception.respond_to?(:detailed_message) + message = exception.detailed_message - # Make the path a bit more readable - if root_pattern and path.sub!(root_pattern, "").nil? - style = :exception_backtrace_other - end + # We want to remove the trailling exception class as we format it differently: + message.sub!(/\s*\(.*?\)$/, '') - output.puts " #{index == 0 ? "→" : " "} #{terminal[style]}#{path}:#{offset}#{terminal[:exception_message]} #{message}#{terminal.reset}" + hash[:message] = message + else + hash[:message] = exception.message end - if exception.cause - format_exception(exception.cause, "Caused by ", output, terminal, verbose) + hash[:backtrace] = exception.backtrace + + if cause = exception.cause + hash[:cause] = Hash.new.tap do |cause_hash| + extract(cause, cause_hash) + end end end end diff --git a/lib/console/event/generic.rb b/lib/console/event/generic.rb index dbb151f..594a061 100644 --- a/lib/console/event/generic.rb +++ b/lib/console/event/generic.rb @@ -1,22 +1,21 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. module Console module Event class Generic - def self.register(terminal) + def as_json(...) + to_hash end - def to_h + def to_json(...) + JSON.generate(as_json, ...) end - def to_json(*arguments) - JSON.generate([self.class, to_h], *arguments) - end - - def format(buffer, terminal) + def emit(*arguments, **options) + Console.call(*arguments, event: self, **options) end end end diff --git a/lib/console/event/progress.rb b/lib/console/event/progress.rb deleted file mode 100644 index 45b6990..0000000 --- a/lib/console/event/progress.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2020-2022, by Samuel Williams. - -require_relative 'generic' - -module Console - module Event - class Progress < Generic - BLOCK = [ - " ", - "▏", - "▎", - "▍", - "▌", - "▋", - "▊", - "▉", - "█", - ] - - def initialize(current, total) - @current = current - @total = total - end - - attr :current - attr :total - - def value - @current.to_f / @total.to_f - end - - def bar(value = self.value, width = 70) - blocks = width * value - full_blocks = blocks.floor - partial_block = ((blocks - full_blocks) * BLOCK.size).floor - - if partial_block.zero? - BLOCK.last * full_blocks - else - "#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}" - end.ljust(width) - end - - def self.register(terminal) - terminal[:progress_bar] ||= terminal.style(:blue, :white) - end - - def to_h - {current: @current, total: @total} - end - - def format(output, terminal, verbose) - output.puts "#{terminal[:progress_bar]}#{self.bar}#{terminal.reset} #{sprintf('%6.2f', self.value * 100)}%" - end - end - end -end diff --git a/lib/console/event/spawn.rb b/lib/console/event/spawn.rb index a4972aa..89fb820 100644 --- a/lib/console/event/spawn.rb +++ b/lib/console/event/spawn.rb @@ -1,12 +1,21 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require_relative 'generic' +require_relative '../clock' module Console module Event + # Represents a spawn event. + # + # ```ruby + # Console.info(self, **Console::Event::Spawn.for("ls", "-l")) + # + # event = Console::Event::Spawn.for("ls", "-l") + # event.status = Process.wait + # ``` class Spawn < Generic def self.for(*arguments, **options) # Extract out the command environment: @@ -22,44 +31,38 @@ def initialize(environment, arguments, options) @environment = environment @arguments = arguments @options = options + + @start_time = Clock.now + + @end_time = nil + @status = nil end - attr :environment - attr :arguments - attr :options - - def chdir_string(options) - if options and chdir = options[:chdir] - " in #{chdir}" + def duration + if @end_time + @end_time - @start_time end end - def self.register(terminal) - terminal[:shell_command] ||= terminal.style(:blue, nil, :bold) - end - - def to_h + def to_hash Hash.new.tap do |hash| + hash[:type] = :spawn hash[:environment] = @environment if @environment&.any? hash[:arguments] = @arguments if @arguments&.any? hash[:options] = @options if @options&.any? + + hash[:status] = @status.to_i if @status + + if duration = self.duration + hash[:duration] = duration + end end end - def format(output, terminal, verbose) - arguments = @arguments.flatten.collect(&:to_s) - - output.puts " #{terminal[:shell_command]}#{arguments.join(' ')}#{terminal.reset}#{chdir_string(options)}" - - if verbose and @environment - @environment.each do |key, value| - output.puts " export #{key}=#{value}" - end - end + def status=(status) + @end_time = Time.now + @status = status end end end - - # Deprecated. - Shell = Event::Spawn end diff --git a/lib/console/filter.rb b/lib/console/filter.rb index 863bf8e..1163c30 100644 --- a/lib/console/filter.rb +++ b/lib/console/filter.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2019, by Bryan Powell. # Copyright, 2020, by Michael Adams. # Copyright, 2021, by Robert Schulze. -require_relative 'buffer' - module Console UNKNOWN = 'unknown' @@ -37,7 +35,7 @@ def self.[] **levels define_immutable_method(name) do |subject = nil, *arguments, **options, &block| if self.enabled?(subject, level) - self.call(subject, *arguments, severity: name, **options, **@options, &block) + self.call(subject, *arguments, severity: name, **@options, **options, &block) end end diff --git a/lib/console/format.rb b/lib/console/format.rb index e73b4fb..8b04972 100644 --- a/lib/console/format.rb +++ b/lib/console/format.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2023, by Samuel Williams. +# Copyright, 2023-2024, by Samuel Williams. require_relative 'format/safe' @@ -10,9 +10,5 @@ module Format def self.default Safe.new(format: ::JSON) end - - def self.default_json - self.default - end end end diff --git a/lib/console/logger.rb b/lib/console/logger.rb index be171f5..8e4078a 100644 --- a/lib/console/logger.rb +++ b/lib/console/logger.rb @@ -1,17 +1,15 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2021, by Bryan Powell. # Copyright, 2021, by Robert Schulze. require_relative 'output' require_relative 'filter' -require_relative 'progress' - +require_relative 'event' require_relative 'resolver' -require_relative 'terminal/logger' -require_relative 'serialized/logger' +require_relative 'progress' require 'fiber/storage' @@ -69,12 +67,24 @@ def initialize(output, **options) end def progress(subject, total, **options) - Progress.new(self, subject, total, **options) + options[:severity] ||= :info + + Progress.new(subject, total, **options) + end + + def error(subject, *arguments, **options, &block) + # This is a special case where we want to create a failure event from an exception. + # It's common to see `Console.error(self, exception)` in code. + if arguments.first.is_a?(Exception) + exception = arguments.shift + options[:event] = Event::Failure.for(exception) + end + + super end - # @deprecated Use `fatal` instead. - def failure(subject, exception, *arguments, &block) - self.fatal(subject, exception, *arguments, &block) + def failure(subject, exception, **options) + error(subject, event: Event::Failure.for(exception), **options) end end end diff --git a/lib/console/output.rb b/lib/console/output.rb index 3e461af..ee8754a 100644 --- a/lib/console/output.rb +++ b/lib/console/output.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2021-2023, by Samuel Williams. +# Copyright, 2021-2024, by Samuel Williams. require_relative 'output/default' -require_relative 'output/json' -require_relative 'output/text' -require_relative 'output/xterm' +require_relative 'output/serialized' +require_relative 'output/terminal' require_relative 'output/null' module Console diff --git a/lib/console/output/default.rb b/lib/console/output/default.rb index 9396af1..2dbcabb 100644 --- a/lib/console/output/default.rb +++ b/lib/console/output/default.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2021-2022, by Samuel Williams. +# Copyright, 2021-2024, by Samuel Williams. -require_relative 'xterm' -require_relative 'json' +require_relative 'terminal' +require_relative 'serialized' module Console module Output @@ -13,9 +13,9 @@ def self.new(output, **options) output ||= $stderr if output.tty? - XTerm.new(output, **options) + Terminal.new(output, **options) else - JSON.new(output, **options) + Serialized.new(output, **options) end end end diff --git a/lib/console/output/json.rb b/lib/console/output/json.rb deleted file mode 100644 index 985cb73..0000000 --- a/lib/console/output/json.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2021-2023, by Samuel Williams. - -require_relative '../serialized/logger' - -module Console - module Output - module JSON - def self.new(output, **options) - Serialized::Logger.new(output, format: Format.default_json, **options) - end - end - end -end diff --git a/lib/console/output/null.rb b/lib/console/output/null.rb index 2605484..9067e0c 100644 --- a/lib/console/output/null.rb +++ b/lib/console/output/null.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2023, by Samuel Williams. +# Copyright, 2023-2024, by Samuel Williams. module Console module Output diff --git a/lib/console/output/sensitive.rb b/lib/console/output/sensitive.rb index 9b2d484..7d91ac3 100644 --- a/lib/console/output/sensitive.rb +++ b/lib/console/output/sensitive.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2021-2022, by Samuel Williams. +# Copyright, 2021-2024, by Samuel Williams. -require_relative '../serialized/logger' +require_relative 'wrapper' module Console module Output - class Sensitive - def initialize(output, **options) - @output = output - end - + class Sensitive < Wrapper REDACT = / phone | email @@ -38,8 +34,14 @@ def initialize(output, **options) | password /xi + def initialize(output, redact: REDACT, **options) + super(output, **options) + + @redact = redact + end + def redact?(text) - text.match?(REDACT) + text.match?(@redact) end def redact_hash(arguments, filter) @@ -96,7 +98,7 @@ def call(subject = nil, *arguments, sensitive: true, **options, &block) arguments = redact_array(arguments, filter) end - @output.call(subject, *arguments, **options) + super(subject, *arguments, **options) end end end diff --git a/lib/console/serialized/logger.rb b/lib/console/output/serialized.rb similarity index 54% rename from lib/console/serialized/logger.rb rename to lib/console/output/serialized.rb index 9482975..4042219 100644 --- a/lib/console/serialized/logger.rb +++ b/lib/console/output/serialized.rb @@ -1,34 +1,23 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. -require_relative '../buffer' -require_relative '../filter' require_relative '../format' - require 'time' - require 'fiber/annotation' module Console - module Serialized - class Logger - def initialize(io = $stderr, format: Format.default, verbose: false, **options) - @io = io - @start = Time.now + module Output + class Serialized + def initialize(output, format: Format.default, **options) + @io = output @format = format - @verbose = verbose end attr :io - attr :start attr :format - def verbose!(value = true) - @verbose = true - end - def dump(record) @format.dump(record) end @@ -72,41 +61,12 @@ def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block) record[:message] = message end - if exception = find_exception(message) - record[:error] = { - kind: exception.class, - message: exception.message, - stack: format_stack(exception) - } - end - record.update(options) @io.puts(self.dump(record)) end - - private - - def find_exception(message) - message.find{|part| part.is_a?(Exception)} - end - - def format_stack(exception) - buffer = StringIO.new - format_backtrace(exception, buffer) - return buffer.string - end - - def format_backtrace(exception, buffer) - buffer.puts exception.backtrace - - if exception = exception.cause - buffer.puts - buffer.puts "Caused by: #{exception.class} #{exception.message}" - - format_backtrace(exception, buffer) - end - end end + + JSON = Serialized end end diff --git a/lib/console/output/split.rb b/lib/console/output/split.rb index 06cada1..d3c3691 100644 --- a/lib/console/output/split.rb +++ b/lib/console/output/split.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2022-2023, by Samuel Williams. +# Copyright, 2022-2024, by Samuel Williams. module Console module Output @@ -18,9 +18,9 @@ def verbose!(value = true) @outputs.each{|output| output.verbose!(value)} end - def call(level, subject = nil, *arguments, **options, &block) + def call(...) @outputs.each do |output| - output.call(level, subject, *arguments, **options, &block) + output.call(...) end end end diff --git a/lib/console/terminal/logger.rb b/lib/console/output/terminal.rb similarity index 65% rename from lib/console/terminal/logger.rb rename to lib/console/output/terminal.rb index c26ab4e..4126698 100644 --- a/lib/console/terminal/logger.rb +++ b/lib/console/output/terminal.rb @@ -1,53 +1,61 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2021, by Robert Schulze. -require_relative '../buffer' -require_relative '../event' require_relative '../clock' - -require_relative 'text' -require_relative 'xterm' +require_relative '../terminal' require 'json' require 'fiber' require 'fiber/annotation' +require 'stringio' module Console - module Terminal - # This, and all related methods, is considered private. - CONSOLE_START_AT = 'CONSOLE_START_AT' - - # Exports CONSOLE_START which can be used to synchronize the start times of all child processes when they log using delta time. - def self.start_at!(environment = ENV) - if time_string = environment[CONSOLE_START_AT] - start_at = Time.parse(time_string) rescue nil + module Output + class Terminal + class Buffer < StringIO + def initialize(prefix = nil) + @prefix = prefix + + super() + end + + attr :prefix + + def puts(*args, prefix: @prefix) + args.each do |arg| + self.write(prefix) if prefix + super(arg) + end + end + + alias << puts end - unless start_at - start_at = Time.now - environment[CONSOLE_START_AT] = start_at.to_s - end + # This, and all related methods, is considered private. + CONSOLE_START_AT = 'CONSOLE_START_AT' - return start_at - end - - def self.for(io) - if io.isatty - XTerm.new(io) - else - Text.new(io) + # Exports CONSOLE_START which can be used to synchronize the start times of all child processes when they log using delta time. + def self.start_at!(environment = ENV) + if time_string = environment[CONSOLE_START_AT] + start_at = Time.parse(time_string) rescue nil + end + + unless start_at + start_at = Time.now + environment[CONSOLE_START_AT] = start_at.to_s + end + + return start_at end - end - - class Logger - def initialize(io = $stderr, verbose: nil, start_at: Terminal.start_at!, format: nil, **options) - @io = io + + def initialize(output, verbose: nil, start_at: Terminal.start_at!, format: nil, **options) + @io = output @start_at = start_at - @terminal = format.nil? ? Terminal.for(io) : format.new(io) + @terminal = format.nil? ? Console::Terminal.for(@io) : format.new(@io) if verbose.nil? @verbose = !@terminal.colors? @@ -66,7 +74,8 @@ def initialize(io = $stderr, verbose: nil, start_at: Terminal.start_at!, format: @terminal[:annotation] = @terminal.reset @terminal[:value] = @terminal.style(:blue) - self.register_defaults(@terminal) + @formatters = {} + self.register_formatters end attr :io @@ -80,20 +89,23 @@ def verbose!(value = true) @verbose = value end - def register_defaults(terminal) - Event.constants.each do |constant| - klass = Event.const_get(constant) - klass.register(terminal) + def register_formatters(namespace = Console::Terminal::Formatter) + namespace.constants.each do |name| + formatter = namespace.const_get(name) + @formatters[formatter::KEY] = formatter.new(@terminal) end end UNKNOWN = :unknown - def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &block) + def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, event: nil, **options, &block) + width = @terminal.width + prefix = build_prefix(name || severity.to_s) indent = " " * prefix.size buffer = Buffer.new("#{indent}| ") + indent_size = buffer.prefix.size format_subject(severity, prefix, subject, buffer) @@ -109,6 +121,10 @@ def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &bl end end + if event + format_event(event, buffer, width - indent_size) + end + if options&.any? format_options(options, buffer) end @@ -118,20 +134,24 @@ def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &bl protected + def format_event(event, buffer, width) + event = event.to_hash + type = event[:type] + + if formatter = @formatters[type] + formatter.format(event, buffer, verbose: @verbose, width: width) + else + format_value(::JSON.pretty_generate(event), buffer) + end + end + def format_options(options, output) format_value(::JSON.pretty_generate(options), output) end def format_argument(argument, output) - case argument - when Exception - Event::Failure.for(argument).format(output, @terminal, @verbose) - when Event::Generic - argument.format(output, @terminal, @verbose) - else - argument.to_s.each_line do |line| - output.puts line - end + argument.to_s.each_line do |line| + output.puts line end end @@ -216,5 +236,17 @@ def build_prefix(name) end end end + + module Text + def self.new(output, **options) + Terminal.new(output, format: Console::Terminal::Text, **options) + end + end + + module XTerm + def self.new(output, **options) + Terminal.new(output, format: Console::Terminal::XTerm, **options) + end + end end end diff --git a/lib/console/output/text.rb b/lib/console/output/text.rb deleted file mode 100644 index 1867a85..0000000 --- a/lib/console/output/text.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2021-2022, by Samuel Williams. - -require_relative '../terminal/logger' - -module Console - module Output - module Text - def self.new(output, **options) - Terminal::Logger.new(output, format: Terminal::Text, **options) - end - end - end -end diff --git a/lib/console/output/wrapper.rb b/lib/console/output/wrapper.rb new file mode 100644 index 0000000..045fec0 --- /dev/null +++ b/lib/console/output/wrapper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2021-2024, by Samuel Williams. + +module Console + module Output + class Wrapper + def initialize(delegate, **options) + @delegate = delegate + end + + def verbose!(value = true) + @delegate.verbose!(value) + end + + def call(...) + @delegate.call(...) + end + end + end +end diff --git a/lib/console/output/xterm.rb b/lib/console/output/xterm.rb deleted file mode 100644 index 293254f..0000000 --- a/lib/console/output/xterm.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2021-2022, by Samuel Williams. - -require_relative '../terminal/logger' - -module Console - module Output - module XTerm - def self.new(output, **options) - Terminal::Logger.new(output, format: Terminal::XTerm, **options) - end - end - end -end diff --git a/lib/console/progress.rb b/lib/console/progress.rb index 7851bf1..d8ab8af 100644 --- a/lib/console/progress.rb +++ b/lib/console/progress.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2020-2023, by Samuel Williams. +# Copyright, 2020-2024, by Samuel Williams. # Copyright, 2022, by Anton Sozontov. -require_relative 'event/progress' require_relative 'clock' module Console @@ -13,9 +12,9 @@ def self.now Process.clock_gettime(Process::CLOCK_MONOTONIC) end - def initialize(output, subject, total = 0, minimum_output_duration: 0.1) - @output = output + def initialize(subject, total = 0, minimum_output_duration: 0.1, **options) @subject = subject + @options = options @start_time = Progress.now @@ -54,11 +53,22 @@ def estimated_remaining_time end end + def to_hash + Hash.new.tap do |hash| + hash[:type] = :progress + hash[:current] = @current + hash[:total] = @total + + hash[:duration] = self.duration + hash[:estimated_remaining_time] = self.estimated_remaining_time + end + end + def increment(amount = 1) @current += amount if output? - @output.info(@subject, self) {Event::Progress.new(@current, @total)} + Console.call(@subject, self.to_s, event: self.to_hash, **@options) @last_output_time = Progress.now end @@ -68,14 +78,14 @@ def increment(amount = 1) def resize(total) @total = total - @output.info(@subject, self) {Event::Progress.new(@current, @total)} + Console.call(@subject, self.to_s, event: self.to_hash, **@options) @last_output_time = Progress.now return self end - def mark(...) - @output.info(@subject, ...) + def mark(*arguments, **options) + Console.call(@subject, *arguments, **options, **@options) end def to_s diff --git a/lib/console/split.rb b/lib/console/split.rb deleted file mode 100644 index 61e5224..0000000 --- a/lib/console/split.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. - -require_relative 'output/split' - -module Console - Split = Output::Split -end diff --git a/lib/console/terminal.rb b/lib/console/terminal.rb index ed3af95..4232520 100644 --- a/lib/console/terminal.rb +++ b/lib/console/terminal.rb @@ -1,6 +1,23 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. -require_relative 'terminal/logger' +require_relative 'terminal/text' +require_relative 'terminal/xterm' + +require_relative 'terminal/formatter/progress' +require_relative 'terminal/formatter/failure' +require_relative 'terminal/formatter/spawn' + +module Console + module Terminal + def self.for(io) + if io.tty? + XTerm.new(io) + else + Text.new(io) + end + end + end +end diff --git a/lib/console/terminal/formatter/failure.rb b/lib/console/terminal/formatter/failure.rb new file mode 100644 index 0000000..14c2a2f --- /dev/null +++ b/lib/console/terminal/formatter/failure.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +module Console + module Terminal + module Formatter + class Failure + KEY = :failure + + def initialize(terminal) + @terminal = terminal + + @terminal[:exception_title] ||= @terminal.style(:red, nil, :bold) + @terminal[:exception_detail] ||= @terminal.style(:yellow) + @terminal[:exception_backtrace] ||= @terminal.style(:red) + @terminal[:exception_backtrace_other] ||= @terminal.style(:red, nil, :faint) + @terminal[:exception_message] ||= @terminal.style(:default) + end + + def format(event, output, prefix: nil, verbose: false, width: 80) + title = event[:class] + message = event[:message] + backtrace = event[:backtrace] + root = event[:root] + + lines = message.lines.map(&:chomp) + + output.puts " #{prefix}#{@terminal[:exception_title]}#{title}#{@terminal.reset}: #{lines.shift}" + + lines.each do |line| + output.puts " #{@terminal[:exception_detail]}#{line}#{@terminal.reset}" + end + + root_pattern = /^#{root}\// if root + + backtrace&.each_with_index do |line, index| + path, offset, message = line.split(":", 3) + style = :exception_backtrace + + # Make the path a bit more readable: + if root_pattern and path.sub!(root_pattern, "").nil? + style = :exception_backtrace_other + end + + output.puts " #{index == 0 ? "→" : " "} #{@terminal[style]}#{path}:#{offset}#{@terminal[:exception_message]} #{message}#{@terminal.reset}" + end + + if cause = event[:cause] + format(cause, output, prefix: "Caused by ", verbose: verbose, width: width) + end + end + end + end + end +end diff --git a/lib/console/terminal/formatter/progress.rb b/lib/console/terminal/formatter/progress.rb new file mode 100644 index 0000000..8a28de9 --- /dev/null +++ b/lib/console/terminal/formatter/progress.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +module Console + module Terminal + module Formatter + class Progress + KEY = :progress + + BLOCK = [ + " ", + "▏", + "▎", + "▍", + "▌", + "▋", + "▊", + "▉", + "█", + ] + + def initialize(terminal) + @terminal = terminal + @terminal[:progress_bar] ||= terminal.style(:blue, :white) + end + + def format(event, output, verbose: false, width: 80) + current = event[:current].to_f + total = event[:total].to_f + value = current / total + + # Clamp value to 1.0 to avoid rendering issues: + if value > 1.0 + value = 1.0 + end + + output.puts "#{@terminal[:progress_bar]}#{self.bar(value, width-10)}#{@terminal.reset} #{sprintf('%6.2f', value * 100)}%" + end + + private + + def bar(value, width) + blocks = width * value + full_blocks = blocks.floor + partial_block = ((blocks - full_blocks) * BLOCK.size).floor + + if partial_block.zero? + BLOCK.last * full_blocks + else + "#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}" + end.ljust(width) + end + end + end + end +end diff --git a/lib/console/terminal/formatter/spawn.rb b/lib/console/terminal/formatter/spawn.rb new file mode 100644 index 0000000..5374995 --- /dev/null +++ b/lib/console/terminal/formatter/spawn.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2024, by Samuel Williams. + +module Console + module Terminal + module Formatter + # Format spawn events. + class Spawn + KEY = :spawn + + def initialize(terminal) + @terminal = terminal + @terminal[:spawn_command] ||= @terminal.style(:blue, nil, :bold) + end + + def format(event, output, verbose: false, width: 80) + environment, arguments, options = event.values_at(:environment, :arguments, :options) + + arguments = arguments.flatten.collect(&:to_s) + + output.puts "#{@terminal[:spawn_command]}#{arguments.join(' ')}#{@terminal.reset}#{chdir_string(options)}" + + if verbose and environment + environment.each do |key, value| + output.puts "export #{key}=#{value}" + end + end + end + + private + + def chdir_string(options) + if options and chdir = options[:chdir] + " in #{chdir}" + end + end + end + end + end +end diff --git a/lib/console/terminal/text.rb b/lib/console/terminal/text.rb index 7499cad..dbaecac 100644 --- a/lib/console/terminal/text.rb +++ b/lib/console/terminal/text.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require 'io/console' @@ -26,6 +26,10 @@ def colors? false end + def width + 80 + end + def style(foreground, background = nil, *attributes) end diff --git a/lib/console/terminal/xterm.rb b/lib/console/terminal/xterm.rb index f4b68ec..809c8d9 100644 --- a/lib/console/terminal/xterm.rb +++ b/lib/console/terminal/xterm.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require 'io/console' @@ -41,6 +41,13 @@ def colors? def size @output.winsize + rescue Errno::ENOTTY + # Fake it... + [24, 80] + end + + def width + size.last end def style(foreground, background = nil, *attributes) diff --git a/lib/console/version.rb b/lib/console/version.rb index c21b621..e63dda3 100644 --- a/lib/console/version.rb +++ b/lib/console/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. module Console VERSION = "1.24.0" diff --git a/test.rb b/test.rb new file mode 100755 index 0000000..ef42c14 --- /dev/null +++ b/test.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require_relative 'lib/console' + +begin + begin + raise ArgumentError, "it failed" + rescue => error + raise RuntimeError, "subsequent error" + end +rescue => error + Console.logger.failure(self, error) +end diff --git a/test/console.rb b/test/console.rb index 933f83f..58fdd25 100644 --- a/test/console.rb +++ b/test/console.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2019, by Bryan Powell. # Copyright, 2020, by Michael Adams. require 'console' -require 'my_module' +require 'console/capture' describe Console do it "has a version number" do @@ -19,41 +19,99 @@ end end - with MyModule do - let(:io) {StringIO.new} - let(:logger) {Console::Logger.new(Console::Terminal::Logger.new(io))} + with 'an isolated logger' do + let(:capture) {Console::Capture.new} + let(:logger) {Console::Logger.new(capture, level: Console::Logger::DEBUG)} - it "should log some messages" do + def around Fiber.new do Console.logger = logger - MyModule.test_logger + super end.resume - - expect(io.string).not.to be(:include?, "GOTO LINE 1") - expect(io.string).to be(:include?, "There be the dragons!") end - it "should show debug messages" do - Fiber.new do - Console.logger = logger - MyModule.logger.debug! - MyModule.test_logger - end.resume - - expect(io.string).to be(:include?, "GOTO LINE 1") + it "can invoke interface methods for all log levels" do + Console::Logger::LEVELS.each do |name, level| + Console.public_send(name, self, "Hello World!", name: "test") + + expect(capture.last).to have_keys( + time: be_a(String), + severity: be == name, + subject: be == self, + arguments: be == ["Hello World!"], + name: be == "test" + ) + end + end + + it "can invoke interface methods for all log levels with block" do + Console::Logger::LEVELS.each do |name, level| + Console.public_send(name, self, name: "test") do + "Hello World!" + end + + expect(capture.last).to have_keys( + time: be_a(String), + severity: be == name, + subject: be == self, + message: be == "Hello World!", + name: be == "test" + ) + end end - it "should log nested exceptions" do - expect(logger.debug?).to be == false - expect(logger.info?).to be == true + it "can invoke interface methods for all log levels with block buffer" do + Console::Logger::LEVELS.each do |name, level| + Console.public_send(name, self, name: "test") do |buffer| + buffer.puts "Hello World!" + end + + expect(capture.last).to have_keys( + time: be_a(String), + severity: be == name, + subject: be == self, + message: be == "Hello World!\n", + name: be == "test" + ) + end + end + + it "can invoke error with exception" do + begin + raise StandardError, "It failed!" + rescue => error + Console.error(self, error, name: "test") + end - Fiber.new do - Console.logger = logger - logger.verbose! - MyModule.log_error - end.resume + expect(capture.last).to have_keys( + time: be_a(String), + severity: be == :error, + subject: be == self, + name: be == "test", + event: have_keys( + type: be == :failure, + message: be == "It failed!", + ), + ) + end + + it "can invoke failure with exception" do + begin + raise StandardError, "It failed!" + rescue => error + Console::Event::Failure.for(error).emit(self, name: "test") + end - expect(io.string).to be(:include?, "Caused by ArgumentError: It broken!") + expect(capture.last).to have_keys( + time: be_a(String), + severity: be == :error, + subject: be == self, + name: be == "test", + event: have_keys( + type: be == :failure, + message: be == "It failed!", + ) + ) end end diff --git a/test/console/compatible/logger.rb b/test/console/compatible/logger.rb index ec9e036..b7ff35b 100644 --- a/test/console/compatible/logger.rb +++ b/test/console/compatible/logger.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2022-2023, by Samuel Williams. +# Copyright, 2022-2024, by Samuel Williams. require 'console/compatible/logger' -require 'console/terminal/logger' +require 'console/output/terminal' describe Console::Compatible::Logger do let(:io) {StringIO.new} - let(:output) {Console::Terminal::Logger.new(io)} + let(:output) {Console::Output::Terminal.new(io)} let(:logger) {Console::Compatible::Logger.new("Test", output)} it "should log messages" do diff --git a/test/console/event/failure.rb b/test/console/event/failure.rb index 4c3dd16..5af0a85 100644 --- a/test/console/event/failure.rb +++ b/test/console/event/failure.rb @@ -2,56 +2,20 @@ # Released under the MIT License. # Copyright, 2021, by Robert Schulze. -# Copyright, 2021-2022, by Samuel Williams. +# Copyright, 2021-2024, by Samuel Williams. require 'console' require 'console/capture' +require 'console/captured_output' class TestError < StandardError def detailed_message(...) - "#{super}\nwith details" + "#{message}\nwith details" end end describe Console::Event::Failure do - let(:output) {StringIO.new} - let(:terminal) {Console::Terminal.for(output)} - - with 'runtime error' do - let(:error) do - RuntimeError.new("Test").tap do |error| - error.set_backtrace([ - "(irb):2:in `rescue in irb_binding'", - "(irb):1:in `irb_binding'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/workspace.rb:114:in `eval'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/workspace.rb:114:in `evaluate'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/context.rb:450:in `evaluate'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:541:in `block (2 levels) in eval_input'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:704:in `signal_status'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:538:in `block in eval_input'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/ruby-lex.rb:166:in `block (2 levels) in each_top_level_statement'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/ruby-lex.rb:151:in `loop'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/ruby-lex.rb:151:in `block in each_top_level_statement'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/ruby-lex.rb:150:in `catch'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb/ruby-lex.rb:150:in `each_top_level_statement'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:537:in `eval_input'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:472:in `block in run'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:471:in `catch'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:471:in `run'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/lib/irb.rb:400:in `start'", - "/path/to/root/.gem/ruby/2.5.7/gems/irb-1.2.7/exe/irb:11:in `'", - "/path/to/root/.gem/ruby/2.5.7/bin/irb:23:in `load'", - "/path/to/root/.gem/ruby/2.5.7/bin/irb:23:in `
'" - ]) - end - end - - it "formats exception removing root path" do - event = Console::Event::Failure.new(error, "/path/to/root") - event.format(output, terminal, true) - expect(output.string.lines[3..-1]).to have_value(be =~ /^\s+\.gem/) - end - end + include_context Console::CapturedOutput with 'test error' do let(:error) do @@ -68,9 +32,27 @@ def detailed_message(...) expect(error.detailed_message).to be =~ /with details/ event = Console::Event::Failure.new(error) - event.format(output, terminal, true) - expect(output.string).to be =~ /Test error!/ - expect(output.string).to be =~ /with details/ + + expect(event.to_hash).to have_keys( + message: be =~ /Test error!\nwith details/ + ) + end + + it "logs error message" do + Console::Event::Failure.for(error).emit(self) + + last = capture.last + expect(last).to have_keys( + severity: be == :error, + subject: be == self, + event: have_keys( + type: be == :failure, + root: be_a(String), + class: be =~ /TestError/, + message: be =~ /Test error!/, + backtrace: be_a(Array), + ) + ) end end end diff --git a/test/console/progress.rb b/test/console/event/progress.rb similarity index 71% rename from test/console/progress.rb rename to test/console/event/progress.rb index 4284464..ee8f021 100644 --- a/test/console/progress.rb +++ b/test/console/event/progress.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2020-2022, by Samuel Williams. +# Copyright, 2020-2024, by Samuel Williams. -require 'console' require 'console/progress' -require 'console/capture' +require 'console/captured_output' describe Console::Progress do - let(:capture) {Console::Capture.new} - let(:logger) {Console::Logger.new(capture)} + include_context Console::CapturedOutput let(:progress) {logger.progress("My Measurement", 100)} with '#mark' do @@ -64,21 +62,13 @@ expect(last).to have_keys( severity: be == :info, subject: be == "My Measurement", - message: be_a(Console::Event::Progress), + arguments: be == ["100/100 completed in 0.0s, 0.0s remaining."], + event: have_keys( + type: be == :progress, + current: be == 100, + total: be == 100, + ), ) end - - it 'can generate a progress bar' do - progress.increment(50) - - last = capture.last - message = last[:message] - - terminal = Console::Terminal::Text.new($stderr) - output = StringIO.new - message.format(output, terminal, true) - - expect(output.string).to be == "███████████████████████████████████ 50.00%\n" - end end end diff --git a/test/console/logger.rb b/test/console/logger.rb index 1b96082..2e199ac 100644 --- a/test/console/logger.rb +++ b/test/console/logger.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2019, by Bryan Powell. # Copyright, 2020, by Michael Adams. # Copyright, 2021, by Robert Schulze. @@ -52,18 +52,6 @@ end end - with '#failure' do - it "logs error message" do - logger.failure(self, StandardError.new("It failed!")) - - last = output.last - expect(last).to have_keys( - severity: be == :fatal, - ) - expect(last[:arguments].first).to be_a(StandardError) - end - end - with "level" do let(:level) {0} let(:logger) {subject.new(output, level: level)} diff --git a/test/console/output.rb b/test/console/output.rb index d13c067..5714796 100644 --- a/test/console/output.rb +++ b/test/console/output.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2021-2023, by Samuel Williams. +# Copyright, 2021-2024, by Samuel Williams. require 'console/logger' require 'console/capture' @@ -17,7 +17,7 @@ let(:capture) {File.open('/tmp/console.log', 'w')} it 'should use a serialized format' do - expect(output).to be_a(Console::Serialized::Logger) + expect(output).to be_a(Console::Output::Serialized) end end @@ -25,15 +25,15 @@ let(:capture) {$stderr} it 'should use a terminal format' do - expect($stderr).to receive(:tty?).and_return(true) + expect($stderr).to receive(:tty?).twice.and_return(true) - expect(output).to be_a Console::Terminal::Logger + expect(output).to be_a Console::Output::Terminal end end with env: {'CONSOLE_OUTPUT' => 'JSON'} do it 'can set output to Serialized and format to JSON' do - expect(output).to be_a Console::Serialized::Logger + expect(output).to be_a Console::Output::Serialized expect(output.format).to be_a(Console::Format::Safe) end end @@ -53,7 +53,7 @@ with env: {'CONSOLE_OUTPUT' => 'XTerm'} do it 'can force format to XTerm for non tty output by ENV' do expect(Console::Terminal).not.to receive(:for) - expect(output).to be_a Console::Terminal::Logger + expect(output).to be_a Console::Output::Terminal expect(output.terminal).to be_a Console::Terminal::XTerm end end @@ -61,7 +61,7 @@ with env: {'CONSOLE_OUTPUT' => 'Text'} do it 'can force format to text for tty output by ENV using Text' do expect(Console::Terminal).not.to receive(:for) - expect(output).to be_a Console::Terminal::Logger + expect(output).to be_a Console::Output::Terminal expect(output.terminal).to be_a Console::Terminal::Text end end diff --git a/test/console/output/default.rb b/test/console/output/default.rb index 9491ec6..771afb9 100644 --- a/test/console/output/default.rb +++ b/test/console/output/default.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2021-2022, by Samuel Williams. +# Copyright, 2021-2024, by Samuel Williams. require 'console/logger' require 'console/capture' describe Console::Output::Default do - let(:output) {Console::Capture.new} + let(:output) {nil} let(:logger) {subject.new(output)} def final_output(output) @@ -18,11 +18,7 @@ def final_output(output) end end - with 'unspecified output' do - let(:output) {nil} - - it 'should output to $stderr by default' do - expect(final_output(logger).io).to be == $stderr - end + it 'should output to $stderr by default' do + expect(final_output(logger).io).to be == $stderr end end diff --git a/test/console/serialized/logger.rb b/test/console/output/serialized.rb similarity index 73% rename from test/console/serialized/logger.rb rename to test/console/output/serialized.rb index fdbbe40..6c36aca 100644 --- a/test/console/serialized/logger.rb +++ b/test/console/output/serialized.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. # Copyright, 2023, by Felix Yan. -require 'console/serialized/logger' +require 'console/output/serialized' require 'console/event/spawn' -describe Console::Serialized::Logger do +describe Console::Output::Serialized do let(:io) {StringIO.new} let(:logger) {subject.new(io)} @@ -37,29 +37,18 @@ let(:event) {Console::Event::Spawn.for("ls -lah")} it "can log structured events" do - logger.call(subject, event) + logger.call(subject, event: event) expect(record).to have_keys( subject: be == subject.name, - message: be == ["Console::Event::Spawn", {:arguments => ["ls -lah"]}] + event: have_keys( + type: be == "spawn", + arguments: be == ["ls -lah"], + ), ) end end - with 'exception' do - let(:error_message) {record[:error]} - - it "can log exception message" do - begin - raise "Boom" - rescue => error - logger.call(self, error) - end - - expect(error_message).to have_keys(:kind, :message, :stack) - end - end - with "Fiber annotation" do it "logs fiber annotations" do Fiber.new do diff --git a/test/console/terminal/logger.rb b/test/console/output/terminal.rb similarity index 90% rename from test/console/terminal/logger.rb rename to test/console/output/terminal.rb index ebadb5e..380da04 100644 --- a/test/console/terminal/logger.rb +++ b/test/console/output/terminal.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. -require 'console/terminal/logger' +require 'console/output/terminal' -describe Console::Terminal::Logger do +describe Console::Output::Terminal do let(:io) {StringIO.new} let(:logger) {subject.new(io, verbose: true)} diff --git a/test/console/terminal/formatter/failure.rb b/test/console/terminal/formatter/failure.rb new file mode 100644 index 0000000..fda3c0b --- /dev/null +++ b/test/console/terminal/formatter/failure.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require 'console/terminal/formatter/failure' +require 'console/event/failure' +require 'console/terminal' + +describe Console::Terminal::Formatter::Failure do + let(:buffer) {StringIO.new} + let(:terminal) {Console::Terminal.for(buffer)} + let(:formatter) {subject.new(terminal)} + + let(:event) do + begin + raise StandardError, "It failed!" + rescue => error + Console::Event::Failure.for(error) + end + end + + it "can format failure events" do + formatter.format(event.to_hash, buffer) + + expect(buffer.string).to be =~ /StandardError: It failed!/ + end +end diff --git a/test/console/terminal/formatter/progress.rb b/test/console/terminal/formatter/progress.rb new file mode 100644 index 0000000..780a34e --- /dev/null +++ b/test/console/terminal/formatter/progress.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require 'console/terminal/formatter/progress' +require 'console/progress' +require 'console/output/null' +require 'console/terminal' + +describe Console::Terminal::Formatter::Progress do + let(:buffer) {StringIO.new} + let(:terminal) {Console::Terminal.for(buffer)} + let(:formatter) {subject.new(terminal)} + + let(:output) {Console::Output::Null.new} + + let(:progress) do + Console::Progress.new(self, 10) + end + + it "can format failure events" do + formatter.format(progress.to_hash, buffer) + + expect(buffer.string).to be =~ /0.00%/ + end +end