diff --git a/.gitignore b/.gitignore index be26b81f..75bf4c75 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /pkg/ /spec/reports/ vendor +*.gem # rspec failure tracking .rspec_status diff --git a/.travis.yml b/.travis.yml index 08dae787..e404d04b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,16 @@ sudo: false language: ruby -rvm: +# rvm: # - "2.0.0" # - "2.1.0" # - "2.2.0" # - rbx - - "2.5.0" +# - "2.5.0" before_install: gem install bundler -v 1.15.4 script: - bundle exec rubocop --version - bundle exec rubocop -D . - bundle exec rspec + - bundle exec ci_system_check.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d6bbd532..4f521c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- `ArduinoInstallation` class for managing lib / executable paths +- `DisplayManager` class for managing Xvfb instance if needed +- `ArduinoCmd` captures and caches preferences +- `ArduinoCmd` reports on whether a board is installed +- `ArduinoCmd` sets preferences +- `ArduinoCmd` installs boards +- `ArduinoCmd` installs libraries +- `ArduinoCmd` selects boards (compiler preference) ### Changed +- `DisplayManger.with_display` doesn't `disable` if the display was enabled prior to starting the block ### Deprecated ### Removed ### Fixed +- Built gems are `.gitignore`d +- Updated gems based on Github's security advisories ### Security diff --git a/arduino_ci.gemspec b/arduino_ci.gemspec index 802886d2..b25f8ba6 100644 --- a/arduino_ci.gemspec +++ b/arduino_ci.gemspec @@ -14,14 +14,17 @@ Gem::Specification.new do |spec| spec.description = spec.description spec.homepage = "http://github.com/ifreecarve/arduino_ci" - spec.files = ['README.md', '.yardopts'] + Dir['lib/**/*.*'].reject { |f| f.match(%r{^(test|spec|features)/}) } - spec.bindir = "exe" + rejection_regex = %r{^(test|spec|features)/} + libfiles = Dir['lib/**/*.*'].reject { |f| f.match(rejection_regex) } + binfiles = Dir[File.join(spec.bindir, '/**/*.*')].reject { |f| f.match(rejection_regex) } + spec.files = ['README.md', '.yardopts'] + libfiles + binfiles + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_development_dependency "bundler", "~> 1.15" spec.add_development_dependency "rspec", "~> 3.0" - spec.add_development_dependency 'rubocop', '~> 0', '>= 0.46.0' - spec.add_development_dependency 'yard', '~>0.8', '>= 0.8' + spec.add_development_dependency 'rubocop', '~>0.49.0' + spec.add_development_dependency 'yard', '~>0.9.11' end diff --git a/exe/ci_system_check.rb b/exe/ci_system_check.rb new file mode 100755 index 00000000..84b565c9 --- /dev/null +++ b/exe/ci_system_check.rb @@ -0,0 +1,44 @@ +require 'arduino_ci' + +puts "Enabling display with display manager" +ArduinoCI::DisplayManager::instance.enable + +puts "Autlocating Arduino command" +arduino_cmd = ArduinoCI::ArduinoCmd.autolocate! + +board_tests = { + "arduino:avr:uno" => true, + "eggs:milk:wheat" => false, +} + +got_problem = false +board_tests.each do |k, v| + puts "I expect arduino_cmd.board_installed?(#{k}) to be #{v}" + result = arduino_cmd.board_installed?(k) + puts " board_installed?(#{k}) returns #{result}. expected #{v}" + got_problem = true if v != result +end + +urls = [ + "https://adafruit.github.io/arduino-board-index/package_adafruit_index.json", + "http://arduino.esp8266.com/stable/package_esp8266com_index.json" +] + +puts "Setting additional URLs" +got_problem = true unless arduino_cmd.set_pref("boardsmanager.additional.urls", urls.join(",")) + +puts "Installing arduino:sam" +got_problem = true unless arduino_cmd.install_board("arduino:sam") +puts "Installing USBHost" +got_problem = true unless arduino_cmd.install_library("USBHost") +puts "checking that library is indexed" +got_problem = true unless arduino_cmd.library_is_indexed +puts "setting compiler warning level" +got_problem = true unless arduino_cmd.set_pref("compiler.warning_level", "all") +puts "use board! (install board)" +got_problem = true unless arduino_cmd.use_board!("arduino:samd:zero") +puts "verify that board has been installed" +got_problem = true unless arduino_cmd.board_installed?("arduino:samd:zero") + +abort if got_problem +exit(0) diff --git a/lib/arduino_ci.rb b/lib/arduino_ci.rb index 7edf9596..2f871c67 100644 --- a/lib/arduino_ci.rb +++ b/lib/arduino_ci.rb @@ -1,124 +1,8 @@ require "arduino_ci/version" - -require 'singleton' - -# Cross-platform way of finding an executable in the $PATH. -# via https://stackoverflow.com/a/5471032/2063546 -# which('ruby') #=> /usr/bin/ruby -def which(cmd) - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] - ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| - exts.each do |ext| - exe = File.join(path, "#{cmd}#{ext}") - return exe if File.executable?(exe) && !File.directory?(exe) - end - end - nil -end +require "arduino_ci/arduino_cmd" # ArduinoCI contains classes for automated testing of Arduino code on the command line # @author Ian Katz module ArduinoCI - # Wrap the Arduino executable. This requires, in some cases, a faked display. - class ArduinoCmd - - # create as many ArduinoCmds as you like, but we need one and only one display manager - class DisplayMgr - include Singleton - attr_reader :enabled - - def initialize - @existing = existing_display? - @enabled = false - @pid = nil - end - - # attempt to determine if the machine is running a graphical display (i.e. not Travis) - def existing_display? - return true if RUBY_PLATFORM.include? "darwin" - return true if ENV["DISPLAY"].nil? - return true if ENV["DISPLAY"].include? ":" - false - end - - # enable a virtual display - def enable - return @enabled = true if @existing # silent no-op if built in display - return unless @pid.nil? - - @enabled = true - @pid = fork do - puts "Forking Xvfb" - system("Xvfb", ":1", "-ac", "-screen", "0", "1280x1024x16") - puts "Xvfb unexpectedly quit!" - end - sleep(3) # TODO: test a connection to the X server? - end - - # disable the virtual display - def disable - return @enabled = false if @existing # silent no-op if built in display - return if @pid.nil? - - begin - Process.kill 9, @pid - ensure - Process.wait @pid - @pid = nil - end - puts "Xvfb killed" - end - - # Enable a virtual display for the duration of the given block - def with_display - enable - begin - yield environment - ensure - disable - end - end - - def environment - return nil unless @existing || @enabled - return {} if @existing - { DISPLAY => ":1.0" } - end - - # On finalize, ensure child process is ended - def self.finalize - disable - end - end - - class << self - protected :new - - # attempt to find a workable Arduino executable across platforms - def guess_executable_location - osx_place = "/Applications/Arduino.app/Contents/MacOS/Arduino" - places = { - "arduino" => !which("arduino").nil?, - osx_place => (File.exist? osx_place), - } - places.each { |k, v| return k if v } - nil - end - - def autolocate - ret = new - ret.path = guess_executable_location - ret - end - end - - attr_accessor :path - - def initialize - @display_mgr = DisplayMgr::instance - end - - end - end diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb new file mode 100644 index 00000000..aee8b6fe --- /dev/null +++ b/lib/arduino_ci/arduino_cmd.rb @@ -0,0 +1,155 @@ +require 'arduino_ci/display_manager' +require 'arduino_ci/arduino_installation' + +module ArduinoCI + + # Wrap the Arduino executable. This requires, in some cases, a faked display. + class ArduinoCmd + + class << self + protected :new + + # @return [ArduinoCmd] A command object with a best guess (or nil) for the installation + def autolocate + new(ArduinoInstallation.autolocate) + end + + # @return [ArduinoCmd] A command object, installing Arduino if necessary + def autolocate! + new(ArduinoInstallation.autolocate!) + end + + end + + attr_accessor :installation + attr_reader :prefs_cache + attr_reader :prefs_response_time + attr_reader :library_is_indexed + + # @param installation [ArduinoInstallation] the location of the Arduino program installation + def initialize(installation) + @display_mgr = DisplayManager::instance + @installation = installation + @prefs_response_time = nil + @prefs_cache = prefs + @library_is_indexed = false + end + + # fetch preferences to a hash + def prefs + resp = nil + @display_mgr.with_display do + start = Time.now + resp = run_and_capture("--get-pref") + @prefs_response_time = Time.now - start + end + return nil unless resp[:success] + lines = resp[:out].split("\n").select { |l| l.include? "=" } + ret = lines.each_with_object({}) do |e, acc| + parts = e.split("=", 2) + acc[parts[0]] = parts[1] + acc + end + ret + end + + # set a preference key/value pair + # @param key [String] the preference key + # @param value [String] the preference value + # @return [bool] whether the command succeeded + def set_pref(key, value) + success = run_with_gui_guess(" about preferences", "--pref", "#{key}=#{value}", "--save-prefs") + @prefs_cache[key] = value if success + success + end + + # run the arduino command + def run(*args, **kwargs) + full_args = [@installation.cmd_path] + args + @display_mgr.run(*full_args, **kwargs) + end + + def run_with_gui_guess(message, *args, **kwargs) + # On Travis CI, we get an error message in the GUI instead of on STDERR + # so, assume that if we don't get a rapid reply that things are not installed + x3 = @prefs_response_time * 3 + Timeout.timeout(x3) do + result = run_and_capture(*args, **kwargs) + result[:success] + end + rescue Timeout::Error + puts "No response in #{x3} seconds. Assuming graphical modal error message#{message}." + false + end + + # run a command and capture its output + # @return [Hash] {:out => String, :err => String, :success => bool} + def run_and_capture(*args, **kwargs) + pipe_out, pipe_out_wr = IO.pipe + pipe_err, pipe_err_wr = IO.pipe + our_kwargs = { out: pipe_out_wr, err: pipe_err_wr } + eventual_kwargs = our_kwargs.merge(kwargs) + success = run(*args, **eventual_kwargs) + pipe_out_wr.close + pipe_err_wr.close + str_out = pipe_out.read + str_err = pipe_err.read + pipe_out.close + pipe_err.close + { out: str_out, err: str_err, success: success } + end + + # run a command and don't capture its output, but use the same signature + # @return [Hash] {:out => String, :err => String, :success => bool} + def run_wrap(*args, **kwargs) + success = run(*args, **kwargs) + { out: "NOPE, use run_and_capture", err: "NOPE, use run_and_capture", success: success } + end + + # check whether a board is installed + # we do this by just selecting a board. + # the arduino binary will error if unrecognized and do a successful no-op if it's installed + def board_installed?(boardname) + run_with_gui_guess(" about board not installed", "--board", boardname) + end + + # install a board by name + # @param name [String] the board name + # @return [bool] whether the command succeeded + def install_board(boardname) + # TODO: find out why IO.pipe fails but File::NULL succeeds :( + run_and_capture("--install-boards", boardname, out: File::NULL)[:success] + end + + # install a library by name + # @param name [String] the library name + # @return [bool] whether the command succeeded + def install_library(library_name) + result = run_and_capture("--install-library", library_name) + @library_is_indexed = true if result[:success] + result[:success] + end + + # update the library index + def update_library_index + # install random lib so the arduino IDE grabs a new library index + # see: https://github.com/arduino/Arduino/issues/3535 + install_library("USBHost") + end + + # use a particular board for compilation + def use_board(boardname) + run_with_gui_guess(" about board not installed", "--board", boardname, "--save-prefs") + end + + # use a particular board for compilation, installing it if necessary + def use_board!(boardname) + return true if use_board(boardname) + boardfamily = boardname.split(":")[0..1].join(":") + puts "Board '#{boardname}' not found; attempting to install '#{boardfamily}'" + return false unless install_board(boardfamily) # guess board family from first 2 :-separated fields + use_board(boardname) + end + + end +end diff --git a/lib/arduino_ci/arduino_installation.rb b/lib/arduino_ci/arduino_installation.rb new file mode 100644 index 00000000..10ef76fd --- /dev/null +++ b/lib/arduino_ci/arduino_installation.rb @@ -0,0 +1,65 @@ +require "arduino_ci/host" + +module ArduinoCI + + # Manage the OS-specific install location of Arduino + class ArduinoInstallation + attr_accessor :cmd_path + attr_accessor :lib_dir + + class << self + def force_install_location + File.join(ENV['HOME'], 'arduino_ci_ide') + end + + # attempt to find a workable Arduino executable across platforms + def autolocate + ret = new + + osx_place = "/Applications/Arduino.app/Contents/MacOS" + if File.exist? osx_place + ret.cmd_path = File.join(osx_place, "Arduino") + ret.lib_dir = File.join(osx_place, "Libraries") + return ret + end + + posix_place = Host.which("arduino") + unless posix_place.nil? + ret.cmd_path = posix_place + ret.lib_dir = File.join(ENV['HOME'], "Sketchbook") # assume linux + # https://learn.adafruit.com/adafruit-all-about-arduino-libraries-install-use/how-to-install-a-library + return ret + end + + if File.exist? force_install_location + ret.cmd_path = File.join(force_install_location, "arduino") + ret.lib_dir = File.join(force_install_location, "libraries") + # TODO: "libraries" is what's in the adafruit install.sh script + return ret + end + + ret + end + + # Attempt to find a workable Arduino executable across platforms, and install it if we don't + def autolocate! + candidate = autolocate + return candidate unless candidate.cmd_path.nil? + # force the install + + if force_install + candidate.cmd_path = File.join(force_install_location, "arduino") + candidate.lib_dir = File.join(force_install_location, "libraries") + end + candidate + end + + def force_install + system("wget", "https://downloads.arduino.cc/arduino-1.6.5-linux64.tar.xz") + system("tar", "xf", "arduino-1.6.5-linux64.tar.xz") + system("mv", "arduino-1.6.5", force_install_location) + end + + end + end +end diff --git a/lib/arduino_ci/display_manager.rb b/lib/arduino_ci/display_manager.rb new file mode 100644 index 00000000..34e15e92 --- /dev/null +++ b/lib/arduino_ci/display_manager.rb @@ -0,0 +1,175 @@ +require 'arduino_ci/host' +require 'singleton' +require 'timeout' + +DESIRED_DISPLAY = ":1.0".freeze + +module ArduinoCI + + # When arduino commands run, they need a graphical display. + # This class handles the setup of that display, if needed. + class DisplayManager + include Singleton + attr_reader :enabled + attr_accessor :debug + + def initialize + @existing = existing_display? + @enabled = false + @pid = nil + @debug = false + + @xv_pipe_out_wr = nil + @xv_pipe_err_wr = nil + @xv_pipe_out = nil + @xv_pipe_err = nil + end + + # attempt to determine if the machine is running a graphical display (i.e. not Travis) + def existing_display? + return true if RUBY_PLATFORM.include? "darwin" + return false if ENV["DISPLAY"].nil? + return true if ENV["DISPLAY"].include? ":" + false + end + + # check whether a process is alive + # https://stackoverflow.com/a/32513298/2063546 + def alive?(pid) + Process.kill(0, pid) + true + rescue + false + end + + # check whether an X server is taking connections + def xserver_exist?(display) + system({ "DISPLAY" => display }, "xdpyinfo", out: File::NULL, err: File::NULL) + end + + # wait for the xvfb command to launch + # @param display [String] the value of the DISPLAY env var + # @param pid [Int] the process of Xvfb + # @param timeout [Int] the timeout in seconds + # @return [Bool] whether we detected a launch + def xvfb_launched?(display, pid, timeout) + Timeout.timeout(timeout) do + loop do + unless alive? pid + puts "Xvfb process has died" + return false + end + x = xserver_exist? display + puts "xdpyinfo reports X server status as #{x}" if debug + return true if x + sleep(0.1) + end + end + rescue Timeout::Error + false + end + + # enable a virtual display + def enable + if @existing + puts "DisplayManager enable: no-op for what appears to be an existing display" if debug + @enabled = true + return + end + + return unless @pid.nil? # TODO: disable first? + @xv_pipe_out.close unless @xv_pipe_out.nil? + @xv_pipe_err.close unless @xv_pipe_err.nil? + + # open Xvfb + xvfb_cmd = [ + "Xvfb", + "+extension", "RANDR", + ":1", + "-ac", + "-screen", "0", + "1280x1024x16", + ] + puts "Xvfb launching" if debug + + @xv_pipe_out, @xv_pipe_out_wr = IO.pipe + @xv_pipe_err, @xv_pipe_err_wr = IO.pipe + pipe = IO.popen(xvfb_cmd, stdout: @xv_pipe_out_wr, err: @xv_pipe_err_wr) + @pid = pipe.pid + @enabled = xvfb_launched?(DESIRED_DISPLAY, @pid, 30) + end + + # disable the virtual display + def disable + if @existing + puts "DisplayManager disable: no-op for what appears to be an existing display" if debug + return @enabled = false + end + + return @enabled = false if @pid.nil? + + # https://www.whatastruggle.com/timeout-a-subprocess-in-ruby + begin + Timeout.timeout(30) do + Process.kill("TERM", @pid) + puts "Xvfb TERMed" if debug + end + rescue Timeout::Error + Process.kill(9, @pid) + puts "Xvfb KILLed" if debug + ensure + Process.wait @pid + @enabled = false + @pid = nil + + @xv_pipe_out_wr.close + @xv_pipe_err_wr.close + end + end + + # Enable a virtual display for the duration of the given block + def with_display + was_enabled = @enabled + enable unless was_enabled + begin + yield environment + ensure + disable unless was_enabled + end + end + + # run a command in a display + def run(*args, **kwargs) + ret = false + # do some work to extract & merge environment variables if they exist + has_env = !args.empty? && args[0].class == Hash + with_display do |env_vars| + env_vars = {} if env_vars.nil? + env_vars.merge!(args[0]) if has_env + actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args + full_cmd = env_vars.empty? ? actual_args : [env_vars] + actual_args + shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ") + puts " $ #{shell_vars} #{actual_args.join(' ')}" + ret = system(*full_cmd, **kwargs) + puts "#{actual_args[0]} has completed" + end + ret + end + + # run a command in a display with no output + def run_silent(*args) + run(*args, out: File::NULL, err: File::NULL) + end + + def environment + return nil unless @existing || @enabled + return { "EXISTING_DISPLAY" => "YES" } if @existing + { "DISPLAY" => DESIRED_DISPLAY } + end + + # On finalize, ensure child process is ended + def self.finalize + disable + end + end +end diff --git a/lib/arduino_ci/host.rb b/lib/arduino_ci/host.rb new file mode 100644 index 00000000..aeed2c2f --- /dev/null +++ b/lib/arduino_ci/host.rb @@ -0,0 +1,19 @@ +module ArduinoCI + + # Tools for interacting with the host machine + class Host + # Cross-platform way of finding an executable in the $PATH. + # via https://stackoverflow.com/a/5471032/2063546 + # which('ruby') #=> /usr/bin/ruby + def self.which(cmd) + exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each do |ext| + exe = File.join(path, "#{cmd}#{ext}") + return exe if File.executable?(exe) && !File.directory?(exe) + end + end + nil + end + end +end diff --git a/spec/arduino_ci_spec.rb b/spec/arduino_ci_spec.rb index 0f3d9c57..e63fca26 100644 --- a/spec/arduino_ci_spec.rb +++ b/spec/arduino_ci_spec.rb @@ -1,7 +1,20 @@ require "spec_helper" RSpec.describe ArduinoCI do - it "has a version number" do - expect(ArduinoCI::VERSION).not_to be nil + context "gem" do + it "has a version number" do + expect(ArduinoCI::VERSION).not_to be nil + end end end + +RSpec.describe ArduinoCI::Host do + context "which" do + it "can find things with which" do + ruby_path = ArduinoCI::Host.which("ruby") + expect(ruby_path).not_to be nil + expect(ruby_path.include? "ruby").to be true + end + end + +end diff --git a/spec/arduino_cmd_spec.rb b/spec/arduino_cmd_spec.rb new file mode 100644 index 00000000..07670fc0 --- /dev/null +++ b/spec/arduino_cmd_spec.rb @@ -0,0 +1,45 @@ +require "spec_helper" + +RSpec.describe ArduinoCI::ArduinoCmd do + context "autolocate" do + it "Finds the Arduino executable" do + arduino_cmd = ArduinoCI::ArduinoCmd.autolocate + end + end + + context "autolocate!" do + it "Finds the Arduino executable" do + arduino_cmd = ArduinoCI::ArduinoCmd.autolocate! + expect(arduino_cmd.installation.cmd_path).not_to be nil + expect(arduino_cmd.prefs_cache.class).to be Hash + expect(arduino_cmd.prefs_response_time).not_to be nil + end + end + + context "board_installed?" do + arduino_cmd = ArduinoCI::ArduinoCmd.autolocate! + ArduinoCI::DisplayManager::instance.enable + it "Finds installed boards" do + uno_installed = arduino_cmd.board_installed? "arduino:avr:uno" + expect(uno_installed).to be true + expect(uno_installed).not_to be nil + end + + it "Doesn't find bogus boards" do + bogus_installed = arduino_cmd.board_installed? "eggs:milk:wheat" + expect(bogus_installed).to be false + expect(bogus_installed).not_to be nil + end + end + + context "set_pref" do + arduino_cmd = ArduinoCI::ArduinoCmd.autolocate! + ArduinoCI::DisplayManager::instance.enable + + it "Sets key to what it was before" do + upload_verify = arduino_cmd.prefs_cache["upload.verify"] + result = arduino_cmd.set_pref("upload.verify", upload_verify) + expect(result).to be true + end + end +end diff --git a/spec/arduino_exec_spec.rb b/spec/arduino_exec_spec.rb deleted file mode 100644 index 19d2147a..00000000 --- a/spec/arduino_exec_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "spec_helper" - -RSpec.describe ArduinoCI::ArduinoCmd do - it "Finds the Arduino executable" do - arduino_cmd = ArduinoCI::ArduinoCmd.autolocate - # expect(arduino_cmd.path).not_to be nil - end -end - -RSpec.describe ArduinoCI::ArduinoCmd::DisplayMgr do - context "singleton ::instance" do - it "produces an instance" do - expect(ArduinoCI::ArduinoCmd::DisplayMgr::instance).not_to be_nil - end - end - - context "with_display" do - it "Properly enables and disables" do - manager = ArduinoCI::ArduinoCmd::DisplayMgr::instance - expect(manager.enabled).to be false - manager.with_display do |environment| - expect(manager.enabled).to be true - expect(environment.class).to eq(Hash) - also_manager = ArduinoCI::ArduinoCmd::DisplayMgr::instance - expect(also_manager.enabled).to be true - end - expect(manager.enabled).to be false - end - end -end diff --git a/spec/arduino_installation_spec.rb b/spec/arduino_installation_spec.rb new file mode 100644 index 00000000..c0a81278 --- /dev/null +++ b/spec/arduino_installation_spec.rb @@ -0,0 +1,25 @@ +require "spec_helper" + +RSpec.describe ArduinoCI::ArduinoInstallation do + context "force_install_location" do + it "is resolvable" do + expect(ArduinoCI::ArduinoInstallation.force_install_location).not_to be nil + end + end + + context "autolocate" do + it "doesn't fail" do + ArduinoCI::ArduinoInstallation.autolocate + end + end + + context "autolocate!" do + it "doesn't fail" do + installation = ArduinoCI::ArduinoInstallation.autolocate! + expect(installation.cmd_path).not_to be nil + expect(installation.lib_dir).not_to be nil + end + end + +end + diff --git a/spec/display_manager_spec.rb b/spec/display_manager_spec.rb new file mode 100644 index 00000000..60b25456 --- /dev/null +++ b/spec/display_manager_spec.rb @@ -0,0 +1,37 @@ +require "spec_helper" + +RSpec.describe ArduinoCI::DisplayManager do + context "singleton ::instance" do + it "produces an instance" do + expect(ArduinoCI::DisplayManager::instance).not_to be_nil + end + end + + context "with_display" do + manager = ArduinoCI::DisplayManager::instance + manager.disable + + it "Properly enables and disables when not previously enabled" do + expect(manager.enabled).to be false + manager.with_display do |environment| + expect(manager.enabled).to be true + expect(environment.class).to eq(Hash) + also_manager = ArduinoCI::DisplayManager::instance + expect(also_manager.enabled).to be true + end + expect(manager.enabled).to be false + end + + it "Properly enables and disables when previously enabled" do + manager.enable + expect(manager.enabled).to be true + manager.with_display do |environment| + expect(manager.enabled).to be true + expect(environment.class).to eq(Hash) + also_manager = ArduinoCI::DisplayManager::instance + expect(also_manager.enabled).to be true + end + expect(manager.enabled).to be true + end + end +end