Skip to content

Commit

Permalink
Merge pull request #876 from joseivanlopez/extend_execute
Browse files Browse the repository at this point in the history
Extend Yast::Execute API
  • Loading branch information
joseivanlopez committed Dec 17, 2018
2 parents 73803e3 + c43b32c commit f0c06b3
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 32 deletions.
174 changes: 157 additions & 17 deletions library/system/src/lib/yast2/execute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

require "yast"
require "cheetah"
require "forwardable"

module Yast
# A module for executing scripts/programs in a safe way
Expand All @@ -30,12 +31,34 @@ module Yast
# as the backend, but adds support for chrooting during the installation.
# It also globally switches the default Cheetah logger to
# {http://www.rubydoc.info/github/yast/yast-ruby-bindings/Yast%2FLogger Y2Logger}.
#
# @example Methods of this class can be chained.
#
# Yast::Execute.locally!.stdout("ls", "-l")
# Yast::Execute.stdout.on_target!("ls", "-l")
class Execute
include Yast::I18n

# use y2log by default
Cheetah.default_options = { logger: Y2Logger.instance }

extend Yast::I18n
textdomain "base"
class << self
extend Forwardable

def_delegators :new, :on_target, :on_target!, :locally, :locally!, :stdout
end

# Constructor
#
# @param options [Hash<Symbol, Object>] options to add for the execution. Some of
# these options are directly passed to Cheetah#run, and others are used to control
# the behavior when running commands (e.g., to indicate if a popup should be shown
# when the command fails). See {#options}.
def initialize(options = {})
textdomain "base"

@options = options
end

# Runs with chroot; a failure becomes a popup.
# Runs a command described by *args*,
Expand All @@ -45,8 +68,8 @@ class Execute
# It also globally switches the default Cheetah logger to
# {http://www.rubydoc.info/github/yast/yast-ruby-bindings/Yast%2FLogger Y2Logger}.
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
def self.on_target(*args)
popup_error { on_target!(*args) }
def on_target(*args)
chaining_object(yast_popup: true).on_target!(*args)
end

# Runs with chroot; a failure becomes an exception.
Expand All @@ -56,16 +79,10 @@ def self.on_target(*args)
# {http://www.rubydoc.info/github/yast/yast-ruby-bindings/Yast%2FLogger Y2Logger}.
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
# @raise Cheetah::ExecutionFailed if the command fails
def self.on_target!(*args)
def on_target!(*args)
root = Yast::WFM.scr_root

if args.last.is_a? ::Hash
args.last[:chroot] = root
else
args.push(chroot: root)
end

Cheetah.run(*args)
chaining_object(chroot: root).run_or_chain(args)
end

# Runs without chroot; a failure becomes a popup.
Expand All @@ -76,8 +93,8 @@ def self.on_target!(*args)
# It also globally switches the default Cheetah logger to
# {http://www.rubydoc.info/github/yast/yast-ruby-bindings/Yast%2FLogger Y2Logger}.
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
def self.locally(*args)
popup_error { locally!(*args) }
def locally(*args)
chaining_object(yast_popup: true).locally!(*args)
end

# Runs without chroot; a failure becomes an exception.
Expand All @@ -87,11 +104,127 @@ def self.locally(*args)
# {http://www.rubydoc.info/github/yast/yast-ruby-bindings/Yast%2FLogger Y2Logger}.
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
# @raise Cheetah::ExecutionFailed if the command fails
def self.locally!(*args)
Cheetah.run(*args)
def locally!(*args)
run_or_chain(args)
end

# Runs a command described by *args* and returns its output
#
# It also globally switches the default Cheetah logger to
# {http://www.rubydoc.info/github/yast/yast-ruby-bindings/Yast%2FLogger Y2Logger}.
#
# @param args [Array<Object>] see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
# @return [String] command output or an empty string if the command fails.
def stdout(*args)
chaining_object(yast_stdout: true, stdout: :capture).run_or_chain(args)
end

protected

# Decides either to run the command or to chain the call in case that no argmuments
# are given.
#
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
# @return [Object, ExecuteClass] result of running the command or a chaining object.
def run_or_chain(args)
args.none? ? self : run(*args)
end

private

# Options to add when running a command
#
# Some options are intended to control the behavior and they are not passed to
# Cheetah.run. For example:
#
# * `yast_popup`: to indicate whether a popup should be shown when the command fails.
# * `yast_stdout`: to indicate whether the command always should return an output,
# even when it fails.
#
# @return [Hash<Symbol, Object>]
attr_reader :options

# New object to chain method calls
#
# The new object contains current object options plus given new options.
#
# @param new_options [Hash<Symbol, Object>]
# @return [ExecuteClass]
def chaining_object(new_options)
self.class.new(options.merge(new_options))
end

# Runs the given command
#
# It takes into account the object options when running the command.
# Note that `yast_popup` takes precedence over `yast_stdout`. So, when both options
# are active and the command fails, a popup error is shown instead of forcing a
# command output. Moreover, when any of such options is active, bang methods like
# {#on_target!} and {#locally!} do not raise an exception.
#
# @example
#
# Yast::Execute.locally.stdout("false") #=> error popup is shown
#
# Yast::Execute.locally!("false") #=> Cheetah::ExecutionFailed
# Yast::Execute.stdout.locally!("false") #=> ""
#
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
def run(*args)
new_args = merge_options(args)

block = proc { Cheetah.run(*new_args) }

if yast_popup?
popup_error(&block)
elsif yast_stdout?
force_stdout(&block)
else
block.call
end
end

# Add object options to the given command
#
# @param args see http://www.rubydoc.info/github/openSUSE/cheetah/Cheetah.run
# @return [Array<Object>]
def merge_options(args)
options = command_options

if options.any?
args << {} unless args.last.is_a?(Hash)
args.last.merge!(options)
end

args
end

# Object options could contain some options to define the behavior when running
# a command (e.g., `yast_popup` and `yast_stdout`). These options are filtered out.
#
# @return [Hash<Symbol, Object>]
def command_options
opts = options.dup

opts.delete_if { |k, _| k.to_s.start_with?("yast") }
end

# Whether `yast_popup` option is active
#
# @return [Boolean]
def yast_popup?
!!options[:yast_popup]
end

private_class_method def self.popup_error(&block)
# Whether `yast_stdout` option is active
#
# @return [Boolean]
def yast_stdout?
!!options[:yast_stdout]
end

# Runs the command and shows a popup when the command fails
def popup_error(&block)
block.call
rescue Cheetah::ExecutionFailed => e
Yast.import "Report"
Expand All @@ -108,5 +241,12 @@ def self.locally!(*args)
}
)
end

# Runs the command and returns an empty string when the command fails
def force_stdout(&block)
block.call
rescue Cheetah::ExecutionFailed
""
end
end
end
102 changes: 88 additions & 14 deletions library/system/test/execute_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,137 @@
expect(Cheetah.default_options[:logger]).to eq Yast::Y2Logger.instance
end

describe ".locally" do
describe "#locally" do
it "returns a chaining object if no argumens are given" do
expect(subject.locally).to be_a(Yast::Execute)
end

it "passes arguments directly to cheetah" do
expect(Cheetah).to receive(:run).with("ls", "-a")

Yast::Execute.locally("ls", "-a")
subject.locally("ls", "-a")
end

it "report error if command execution failed" do
expect(Yast::Report).to receive(:Error)
Yast::Execute.locally("false")
subject.locally("false")
end

it "returns nil if command execution failed" do
expect(Yast::Execute.locally("false")).to eq nil
expect(subject.locally("false")).to eq nil
end
end

describe ".locally!" do
describe "#locally!" do
it "returns a chaining object if no argumens are given" do
expect(subject.locally).to be_a(Yast::Execute)
end

it "passes arguments directly to cheetah" do
expect(Cheetah).to receive(:run).with("ls", "-a")

Yast::Execute.locally("ls", "-a")
subject.locally("ls", "-a")
end

it "raises Cheetah::ExecutionFailed if command execution failed" do
expect { Yast::Execute.locally!("false") }.to raise_error(Cheetah::ExecutionFailed)
expect { subject.locally!("false") }.to raise_error(Cheetah::ExecutionFailed)
end
end

describe ".on_target" do
describe "#on_target" do
it "returns a chaining object if no argumens are given" do
expect(subject.on_target).to be_a(Yast::Execute)
end

it "adds to passed arguments chroot option if scr chrooted" do
allow(Yast::WFM).to receive(:scr_root).and_return("/mnt")
expect(Cheetah).to receive(:run).with("ls", "-a", chroot: "/mnt")

Yast::Execute.on_target("ls", "-a")
subject.on_target("ls", "-a")
end

it "report error if command execution failed" do
expect(Yast::Report).to receive(:Error)
Yast::Execute.on_target("false")
subject.on_target("false")
end

it "returns nil if command execution failed" do
expect(Yast::Execute.on_target("false")).to eq nil
expect(subject.on_target("false")).to eq nil
end
end

describe ".on_target!" do
describe "#on_target!" do
it "returns a chaining object if no argumens are given" do
expect(subject.on_target!).to be_a(Yast::Execute)
end

it "adds to passed arguments chroot option if scr chrooted" do
allow(Yast::WFM).to receive(:scr_root).and_return("/mnt")
expect(Cheetah).to receive(:run).with("ls", "-a", chroot: "/mnt")

Yast::Execute.on_target("ls", "-a")
subject.on_target("ls", "-a")
end

it "raises Cheetah::ExecutionFailed if command execution failed" do
expect { Yast::Execute.on_target!("false") }.to raise_error(Cheetah::ExecutionFailed)
expect { subject.on_target!("false") }.to raise_error(Cheetah::ExecutionFailed)
end
end

describe "#stdout" do
it "returns a chaining object if no argumens are given" do
expect(subject.stdout).to be_a(Yast::Execute)
end

it "captures stdout of the command" do
expect(Cheetah).to receive(:run).with("ls", "-a", stdout: :capture)

subject.stdout("ls", "-a")
end

it "returns an empty string if command execution failed" do
expect(subject.stdout("false")).to eq("")
end

context "when chaining with #locally" do
it "report error if command execution failed" do
expect(Yast::Report).to receive(:Error)
subject.locally.stdout("false")
end

it "returns nil if command execution failed" do
expect(subject.locally.stdout("false")).to be_nil
end
end

context "when chaining with #on_target" do
it "report error if command execution failed" do
expect(Yast::Report).to receive(:Error)
subject.on_target.stdout("false")
end

it "returns nil if command execution failed" do
expect(subject.on_target.stdout("false")).to be_nil
end
end

context "when chaining with #locally!" do
it "does not raise an exception if command execution failed" do
expect { subject.locally!.stdout("false") }.to_not raise_error
end

it "returns an empty string if command execution failed" do
expect(subject.locally!.stdout("false")).to eq("")
end
end

context "when chaining with #on_target!" do
it "does not raise an exception if command execution failed" do
expect { subject.on_target!.stdout("false") }.to_not raise_error
end

it "returns an empty string if command execution failed" do
expect(subject.on_target!.stdout("false")).to eq("")
end
end
end
end
8 changes: 8 additions & 0 deletions package/yast2.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
-------------------------------------------------------------------
Mon Dec 17 08:09:10 UTC 2018 - jlopez@suse.com

- Extend Yast::Execute API (needed for bsc#1118291)
- Add method Yast::Execute.stdout
- Allow to chain methods
- 4.1.42

-------------------------------------------------------------------
Mon Dec 17 07:23:47 UTC 2018 - Ancor Gonzalez Sosa <ancor@suse.com>

Expand Down
Loading

0 comments on commit f0c06b3

Please sign in to comment.