Skip to content

Commit

Permalink
Making shell commands more robust (with potential benefit to a runnin…
Browse files Browse the repository at this point in the history
…g game).
  • Loading branch information
RoUS committed Mar 17, 2024
1 parent b1f6f6c commit 54990e8
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 29 deletions.
89 changes: 84 additions & 5 deletions lib/tagf/logging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,65 @@ module Tools

include(TAGF::Exceptions)

module Definitions

extend(Forwardable)

# A `#maxlevel` of `-2` is intended to mean that all messages
# should be suppressed unless they're reporting actual severe
# issues.
SUPPRESS_REPORTS = -2

# A `#maxlevel` of `-1` is used to suppress reporting of any
# messages that aren't dealing with actual error conditions, as
# opposed to detailed 'what's going on' debugging into.
REPORT_ONLY_ERRORS = -1

# Verbosity level to be attached to a call to Reporter#report
# when the message is about a legitimate error rather than just
# verbose debugging info.
ERROR_REPORT = REPORT_ONLY_ERRORS

def_delegators(TAGF::Tools, :logger, :logger=)

nil
end # module TAGF::Tools::Definitions

# @!attribute [rw] logger
# When configured, the TAGF::Tools.logger attribute provides an
# application-wide means of reporting messages to `stderr`.
# Typically, is it set up when an instance of
# TAGF::Tools::Reporter is instantiated with a keyword argument of
# `install: true`, but it can be directly installed at any time by
# setting the attribute directly.
#
# @example Setting as part of creating a new Reporter
# TAGF::Tools::Reporter.new(install: true)
# @example Installing after Reporter creation
# reporter = TAGF::Tools::Reporter.new(maxlevel: 4)
# TAGF::Tools.logger = reporter
#
# @return [TAGF::Tools::Reporter]
attr_accessor(:logger)
module_function(:logger)
module_function(:logger=)

# Class
class Reporter

include(TAGF::Exceptions)
include(TAGF::Tools::Definitions)

# @!attribute [rw] component
#
# Component for which loggingg is being performed. If
# non-`nil`, it will be prefixed as
# <tt>"<<em>component</em>>: #"</tt> to each message emitted,
# This can be disabled on a case-by-case basis by including
# `component: false` in the #report `kwargs`.
#
# @return [String]
attr_accessor(:component)

# @!attribute [rw] maxlevel
#
Expand Down Expand Up @@ -106,12 +158,14 @@ def worst_severity
# @param [Hash<Symbol=>Any>] kwargs
# @option kwargs [Integer] :level
# @option kwargs [String] :message
# @option kwargs [Boolean] :prefix
# @option kwargs [String] :component
# @option kwargs [String] :format
# @option kwargs [Array] :fmtargs
#
# @return [void]
def report(*args, **kwargs)
return nil if (self.maxlevel < 0)
return nil if (self.maxlevel <= SUPPRESS_REPORTS)
if (args[0].kind_of?(Integer))
msglevel = args.shift
end
Expand All @@ -123,8 +177,17 @@ def report(*args, **kwargs)
# All right, this message is eligible for reporting. Now
# just figure out what the message *is*.
#
msg = ''
if (kwargs.fetch(:prefix, true))
pfx = kwargs[:component] || self.component
if (pfx.kind_of?(String) &&
(! pfx.empty?))
msg = pfx + ': '
end
else
msg = ''
end
if (fmt = kwargs[:format])
fmt = msg + fmt
#
# If we were passed a format string, use it (and any
# arguments supplied for it) to generate the message text.
Expand All @@ -139,14 +202,14 @@ def report(*args, **kwargs)
# No formatting; see if the keyword args include a string
# to use.
#
msg = kwargs[:message]
msg += kwargs[:message]
else
#
# If all else fails, use the first positional argument we
# were passed. Make sure we stringify it just in case
# it's not a string, or nothing was passed at all.
#
msg = args[0].to_s
msg += args[0].to_s
end
warn(msg)
#
Expand All @@ -159,13 +222,29 @@ def report(*args, **kwargs)
return nil
end # def report(*args, **kwargs)

# @!method initialize(*args, **kwargs)
# Constructor for TAGF::Tools::Reporter class.
#
# @param [Array] args
# @param [Hash<Symbol=>Any>] kwargs
# @option kwargs [String] :component (nil)
# Name of the tool or component that should be prefixed to
# every message string by default.
# @option kwargs [Boolean] :install (false)
# Whether or not the new Reporter instance should be installed
# in the module-wide TAGF::Tools#logger attribute.
# @option kwargs [Integer] :maxlevel (0)
# The maximum detail level of messages to be reported. A
# value of -
def initialize(*args, **kwargs)
self.reset
self.logger = self if (kwargs[:install])
if (kwargs[:quiet])
self.maxlevel = -1
self.maxlevel = SUPPRESS_REPORTS
else
self.maxlevel = (kwargs[:maxlevel] || 0).to_i
end
self.component = kwargs.fetch(:component, nil)
end # def initialize

nil
Expand Down
129 changes: 110 additions & 19 deletions lib/tagf/tools/render.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
require('tagf/cli')
require('tagf/exceptions')
require('tagf/filer')
require('tagf/logging')
require('abbrev')
require('pathname')
require('rgl/dot')
require('ruby-graphviz')
Expand Down Expand Up @@ -61,6 +63,11 @@ module Tools
# be used.
#

Orientations = Abbrev.abbrev([
'portrait',
'landscape',
])

# @!method render(**kwargs)
# Generate a graphic depiction of the game map, either from an
# actual game object or from a `YAML` file defining it. The
Expand All @@ -77,14 +84,25 @@ module Tools
# Exit code (on success, Errno::NOERROR.new.errno (zero)). On
# failure, either -1 or whatever exception processing delivers.
def render(**kwargs)
verbosity = kwargs[:verbosity].to_i
verbosity = SUPPRESS_REPORTS if (kwargs[:quiet])
logger = Reporter.new(maxlevel: verbosity,
component: __callee__.to_s)
output_default = ''
if (game = kwargs[:game])
logger.report(format('validating pre-loaded game "%s"',
game.eid),
level: 1)
#
# If we're working from a Game object, the default output
# filename is the EID.
#
output_default = game.eid
elsif (source = kwargs[:source])
logger.report(format('preparing to render game ' +
'loaded from "%s"',
source),
level: 1)
#
# If we're working from a source file, then the default output
# filename is derived from the source minus any extension.
Expand All @@ -102,27 +120,67 @@ def render(**kwargs)
output = kwargs[:output] || output_default
gformat = kwargs[:format] || 'png'
#
# If the user specified a file with the graphic format as the
# extension, strip it off, since the graph-writing code
# unconditionally adds it.
#
output = output.sub(%r!\.#{gformat}$!, '')
#
# Assume we're going to be successful.
#
result = Errno::NOERROR.new.errno
catch(:render_done) do
#
# If we're given a bogus graphic format, gritch about it.
#
unless (GraphViz::Constants::FORMATS.include?(gformat))
badfmtmsg = 'unknown/unsupported graphic format "%s"'
badfmtexc = RuntimeError.new(format(badfmtmsg, gformat))
warn(format('%s, %s', badfmtexc.class.to_s, badfmtmsg))
result = -1
throw(:render_done)
end
begin
#
# There may be something bogus in the game itself, or the
# graph processing might raise an exception.
#
logger.report(format('building game digraph for "%s"',
game.to_key),
level: 2)
game.graphinfo.assemble
game.graphinfo.graph.write_to_graphic_file(gformat, output)
#
# Fill in any graph-wide options (which use a hash with
# string keys, not symbols) from what we've collected.
#
# The internal name of the graph is *always* the game's EID.
#
logger.report('applying graph-wide attributes',
level: 3)
dotoptions = {
'name' => game.eid,
}
#
# If the user specified a name on the command line (or in
# the options kwargs if invoked any other way), it's meant
# to be as the graph's label.
#
if (optval = kwargs[:name])
dotoptions['label'] = optval
end
#
# The orientation can be either landscape or portrait (the
# default).
#
if (optval = kwargs[:orientation])
optval = TAGF::Tools::Orientations[optval]
dotoptions['orientation'] = optval
end
#
# @todo
# --pagesize is NYI
#
if (optval = kwargs[:pagesize])
dotoptions['page'] = optval.sub(%r!x!i, ',')
end
logger.report(format('writing %s graph rendition to "%s.%s"',
gformat.upcase,
output,
gformat),
level: 0)
game.graphinfo.graph.write_to_graphic_file(gformat,
output,
dotoptions)
rescue StandardError => exc
#
# For now, this is essentially a no-op. However, we might
Expand Down Expand Up @@ -151,6 +209,7 @@ def render(**kwargs)
end

TAGF::CLI.command('render') do |cdef,**opts|

cdef.summary('Render an image of a game map.')
cdef.usage('render [options] [sourcefile]')
cdef.description(<<-EOT)
Expand All @@ -174,8 +233,11 @@ def render(**kwargs)
end
cdef.flag(:v,
:verbose,
'Increase verbosity',
'Each occurrence increases detail of reporting',
multiple: true)
cdef.flag(:q,
:quiet,
'Suppress *all* messages')
# cdef.param(:sourcefile)
cdef.option(:s,
:source,
Expand All @@ -190,7 +252,42 @@ def render(**kwargs)
:format,
'image format (e.g., "svg", "png", "jpg")',
argument: :optional,
default: 'png')
default: 'png') do |value,cmd|
#
# If we're given a bogus graphic format, gritch about it.
#
unless (GraphViz::Constants::FORMATS.include?(value))
badfmtmsg = 'unknown/unsupported graphic format "%s"'
badfmtexc = RuntimeError.new(format(badfmtmsg, value))
warn(format('%s, %s', badfmtexc.class.to_s, badfmtmsg))
exit(1)
end
end

cdef.option(:c,
:config,
('YAML file customising graph display attributes. ' +
'(See the "graph-attributes.yml" file in this gem ' +
'for an example and the defaults.)'),
argument: :required)
cdef.option(:n,
:name,
('Label for the graph as a whole.'),
argument: :required)
cdef.option(nil,
:orientation,
('portrait (default) or landscape.'),
argument: :required,
transform: -> (val) { TAGF::Tools::Orientations[val] }
) do |value,cmd|
unless (optval = TAGF::Tools::Orientations[value])
warn(format('%s: bad value for orientation: %s',
cmd.name,
value.inspect))
exit(1)
end
optval
end

cdef.run do |opts,args,cmd|
opts[:verbosity] = [*opts.delete(:verbose)].count
Expand All @@ -204,12 +301,6 @@ def render(**kwargs)
end
opts[:source] = input
result = TAGF::Tools.render(**opts)
warn(format("%s.%s\n opts = %s\n args = %s\n cmd = %s",
self.respond_to?(:name) ? self.name : self.class.to_s,
__callee__.to_s,
opts.inspect,
args.inspect,
cmd.inspect))
result
end
end
Expand Down
Loading

0 comments on commit 54990e8

Please sign in to comment.