require "spec_helper"
require "etc"
require "logger"
require "timeout"

describe Mixlib::ShellOut do
  let(:shell_cmd) { options ? shell_cmd_with_options : shell_cmd_without_options }
  let(:executed_cmd) { shell_cmd.tap(&:run_command) }
  let(:stdout) { executed_cmd.stdout }
  let(:stderr) { executed_cmd.stderr }
  let(:chomped_stdout) { stdout.chomp }
  let(:stripped_stdout) { stdout.strip }
  let(:exit_status) { executed_cmd.status.exitstatus }

  let(:shell_cmd_without_options) { Mixlib::ShellOut.new(cmd) }
  let(:shell_cmd_with_options) { Mixlib::ShellOut.new(cmd, options) }
  let(:cmd) { ruby_eval.call(ruby_code) }
  let(:ruby_code) { raise "define let(:ruby_code)" }
  let(:options) { nil }

  let(:ruby_eval) { lambda { |code| "ruby -e '#{code}'" } }

  context "when instantiating" do
    subject { shell_cmd }
    let(:cmd) { "apt-get install chef" }

    it "should set the command" do
      expect(subject.command).to eql(cmd)
    end

    context "with default settings" do
      describe "#cwd" do
        subject { super().cwd }
        it { is_expected.to be_nil }
      end

      describe "#user" do
        subject { super().user }
        it { is_expected.to be_nil }
      end

      describe "#with_logon" do
        subject { super().with_logon }
        it { is_expected.to be_nil }
      end

      describe "#login" do
        subject { super().login }
        it { is_expected.to be_nil }
      end

      describe "#domain" do
        subject { super().domain }
        it { is_expected.to be_nil }
      end

      describe "#password" do
        subject { super().password }
        it { is_expected.to be_nil }
      end

      describe "#group" do
        subject { super().group }
        it { is_expected.to be_nil }
      end

      describe "#umask" do
        subject { super().umask }
        it { is_expected.to be_nil }
      end

      describe "#timeout" do
        subject { super().timeout }
        it { is_expected.to eql(600) }
      end

      describe "#valid_exit_codes" do
        subject { super().valid_exit_codes }
        it { is_expected.to eql([0]) }
      end

      describe "#live_stream" do
        subject { super().live_stream }
        it { is_expected.to be_nil }
      end

      describe "#input" do
        subject { super().input }
        it { is_expected.to be_nil }
      end

      describe "#cgroup" do
        subject { super().input }
        it { is_expected.to be_nil }
      end

      it "should not set any default environmental variables" do
        expect(shell_cmd.environment).to eq({})
      end
    end

    context "when setting accessors" do
      subject { shell_cmd.send(accessor) }

      let(:shell_cmd) { blank_shell_cmd.tap(&with_overrides) }
      let(:blank_shell_cmd) { Mixlib::ShellOut.new("apt-get install chef") }
      let(:with_overrides) { lambda { |shell_cmd| shell_cmd.send("#{accessor}=", value) } }

      context "when setting user" do
        let(:accessor) { :user }
        let(:value) { "root" }

        it "should set the user" do
          is_expected.to eql(value)
        end

        # TODO add :unix_only
        context "with an integer value for user" do
          let(:value) { 0 }
          it "should use the user-supplied uid" do
            expect(shell_cmd.uid).to eql(value)
          end
        end

        # TODO add :unix_only
        context "with string value for user" do
          let(:value) { username }

          let(:username) { user_info.name }
          let(:expected_uid) { user_info.uid }
          let(:user_info) { Etc.getpwent }

          it "should compute the uid of the user", :unix_only do
            expect(shell_cmd.uid).to eql(expected_uid)
          end
        end
      end

      context "when setting with_logon" do
        let(:accessor) { :with_logon }
        let(:value) { "root" }

        it "should set the with_logon" do
          is_expected.to eql(value)
        end
      end

      context "when setting login" do
        let(:accessor) { :login }
        let(:value) { true }

        it "should set the login" do
          is_expected.to eql(value)
        end
      end

      context "when setting domain" do
        let(:accessor) { :domain }
        let(:value) { "localhost" }

        it "should set the domain" do
          is_expected.to eql(value)
        end
      end

      context "when setting password" do
        let(:accessor) { :password }
        let(:value) { "vagrant" }

        it "should set the password" do
          is_expected.to eql(value)
        end
      end

      context "when setting group" do
        let(:accessor) { :group }
        let(:value) { "wheel" }

        it "should set the group" do
          is_expected.to eql(value)
        end

        # TODO add :unix_only
        context "with integer value for group" do
          let(:value) { 0 }
          it "should use the user-supplied gid" do
            expect(shell_cmd.gid).to eql(value)
          end
        end

        context "with string value for group" do
          let(:value) { groupname }
          let(:groupname) { group_info.name }
          let(:expected_gid) { group_info.gid }
          let(:group_info) { Etc.getgrent }

          it "should compute the gid of the user", :unix_only do
            expect(shell_cmd.gid).to eql(expected_gid)
          end
        end
      end

      context "when setting the umask" do
        let(:accessor) { :umask }

        context "with octal integer" do
          let(:value) { 007555 }

          it "should set the umask" do
            is_expected.to eql(value)
          end
        end

        context "with decimal integer" do
          let(:value) { 2925 }

          it "should sets the umask" do
            is_expected.to eql(005555)
          end
        end

        context "with string" do
          let(:value) { "7777" }

          it "should sets the umask" do
            is_expected.to eql(007777)
          end
        end
      end

      context "when setting read timeout" do
        let(:accessor) { :timeout }
        let(:value) { 10 }

        it "should set the read timeout" do
          is_expected.to eql(value)
        end
      end

      context "when setting valid exit codes" do
        let(:accessor) { :valid_exit_codes }
        let(:value) { [0, 23, 42] }

        it "should set the valid exit codes" do
          is_expected.to eql(value)
        end
      end

      context "when setting a live stream" do
        let(:accessor) { :live_stream }
        let(:value) { stream }
        let(:stream) { StringIO.new }

        before(:each) do
          shell_cmd.live_stream = stream
        end

        it "live stream should return the stream used for live stdout and live stderr" do
          expect(shell_cmd.live_stream).to eql(stream)
        end

        it "should set the live stdout stream" do
          expect(shell_cmd.live_stderr).to eql(stream)
        end

        it "should set the live stderr stream" do
          expect(shell_cmd.live_stderr).to eql(stream)
        end
      end

      context "when setting the live stdout and live stderr streams separately" do
        let(:accessor) { :live_stream }
        let(:stream) { StringIO.new }
        let(:value) { stream }
        let(:stdout_stream) { StringIO.new }
        let(:stderr_stream) { StringIO.new }

        before(:each) do
          shell_cmd.live_stdout = stdout_stream
          shell_cmd.live_stderr = stderr_stream
        end

        it "live_stream should return nil" do
          expect(shell_cmd.live_stream).to be_nil
        end

        it "should set the live stdout" do
          expect(shell_cmd.live_stdout).to eql(stdout_stream)
        end

        it "should set the live stderr" do
          expect(shell_cmd.live_stderr).to eql(stderr_stream)
        end
      end

      context "when setting a live stream and then overriding the live stderr" do
        let(:accessor) { :live_stream }
        let(:value) { stream }
        let(:stream) { StringIO.new }

        before(:each) do
          shell_cmd.live_stdout = stream
          shell_cmd.live_stderr = nil
        end

        it "should return nil" do
          is_expected.to be_nil
        end

        it "should set the live stdout" do
          expect(shell_cmd.live_stdout).to eql(stream)
        end

        it "should set the live stderr" do
          expect(shell_cmd.live_stderr).to eql(nil)
        end
      end

      context "when setting an input" do
        let(:accessor) { :input }
        let(:value) { "Random content #{rand(1000000)}" }

        it "should set the input" do
          is_expected.to eql(value)
        end
      end

      context "when setting cgroup" do
        let(:accessor) { :cgroup }
        let(:value) { "test" }

        it "should set the cgroup" do
          is_expected.to eql(value)
        end
      end
    end

    context "testing login", :unix_only do
      subject { shell_cmd }
      let(:uid) { 1005 }
      let(:gid) { 1002 }
      let(:shell) { "/bin/money" }
      let(:dir) { "/home/castle" }
      let(:path) { "/sbin:/bin:/usr/sbin:/usr/bin" }
      before :each do
        shell_cmd.login = true
        catbert_user = double("Etc::Passwd", name: "catbert", passwd: "x", uid: 1005, gid: 1002, gecos: "Catbert,,,", dir: "/home/castle", shell: "/bin/money")
        group_double = [
          double("Etc::Group", name: "catbert", passwd: "x", gid: 1002, mem: []),
          double("Etc::Group", name: "sudo", passwd: "x", gid: 52, mem: ["catbert"]),
          double("Etc::Group", name: "rats", passwd: "x", gid: 43, mem: ["ratbert"]),
          double("Etc::Group", name: "dilbertpets", passwd: "x", gid: 700, mem: %w{catbert ratbert}),
        ]
        allow(Etc).to receive(:getpwuid).with(1005) { catbert_user }
        allow(Etc).to receive(:getpwnam).with("catbert") { catbert_user }
        allow(shell_cmd).to receive(:all_seconderies) { group_double }
      end

      # Setting the user by name should change the uid
      context "when setting user by name" do
        before(:each) { shell_cmd.user = "catbert" }
        describe "#uid" do
          subject { super().uid }
          it { is_expected.to eq(uid) }
        end
      end

      context "when setting user by id" do
        before(:each) { shell_cmd.user = uid }
        # Setting the user by uid should change the uid
        # it 'should set the uid' do

        describe "#uid" do
          subject { super().uid }
          it { is_expected.to eq(uid) }
        end
        # end
        # Setting the user without a different gid should change the gid to 1002

        describe "#gid" do
          subject { super().gid }
          it { is_expected.to eq(gid) }
        end
        # Setting the user and the group (to 43) should change the gid to 43
        context "when setting the group manually" do
          before(:each) { shell_cmd.group = 43 }

          describe "#gid" do
            subject { super().gid }
            it { is_expected.to eq(43) }
          end
        end
        # Setting the user should set the env variables
        describe "#process_environment" do
          subject { super().process_environment }
          it { is_expected.to eq({ "HOME" => dir, "SHELL" => shell, "USER" => "catbert", "LOGNAME" => "catbert", "PATH" => path, "IFS" => "\t\n" }) }
        end
        # Setting the user with overriding env variables should override
        context "when adding environment variables" do
          before(:each) { shell_cmd.environment = { "PATH" => "/lord:/of/the/dance", "CUSTOM" => "costume" } }
          it "should preserve custom variables" do
            expect(shell_cmd.process_environment["PATH"]).to eq("/lord:/of/the/dance")
          end
          # Setting the user with additional env variables should have both
          it "should allow new variables" do
            expect(shell_cmd.process_environment["CUSTOM"]).to eq("costume")
          end
        end
        # Setting the user should set secondary groups
        describe "#sgids" do
          subject { super().sgids }
          it { is_expected.to match_array([52, 700]) }
        end
      end
      # Setting login with user should throw errors
      context "when not setting a user id" do
        it "should fail showing an error" do
          expect { Mixlib::ShellOut.new("hostname", { login: true }) }.to raise_error(Mixlib::ShellOut::InvalidCommandOption)
        end
      end
    end

    context "with options hash" do
      let(:cmd) { "brew install couchdb" }
      let(:options) do
        { cwd:, user:, login: true, domain:, password:, group:,
          umask:, timeout:, environment:, returns: valid_exit_codes,
          live_stream: stream, input:, cgroup: }
      end

      let(:cwd) { "/tmp" }
      let(:user) { "toor" }
      let(:with_logon) { user }
      let(:login) { true }
      let(:domain) { "localhost" }
      let(:password) { "vagrant" }
      let(:group) { "wheel" }
      let(:umask) { "2222" }
      let(:timeout) { 5 }
      let(:environment) { { "RUBY_OPTS" => "-w" } }
      let(:valid_exit_codes) { [ 0, 1, 42 ] }
      let(:stream) { StringIO.new }
      let(:input) { 1.upto(10).map { "Data #{rand(100000)}" }.join("\n") }
      let(:cgroup) { "test" }

      it "should set the working directory" do
        expect(shell_cmd.cwd).to eql(cwd)
      end

      it "should set the user" do
        expect(shell_cmd.user).to eql(user)
      end

      it "should set the with_logon" do
        expect(shell_cmd.with_logon).to eql(with_logon)
      end

      it "should set the login" do
        expect(shell_cmd.login).to eql(login)
      end

      it "should set the domain" do
        expect(shell_cmd.domain).to eql(domain)
      end

      it "should set the password" do
        expect(shell_cmd.password).to eql(password)
      end

      it "should set the group" do
        expect(shell_cmd.group).to eql(group)
      end

      it "should set the umask" do
        expect(shell_cmd.umask).to eql(002222)
      end

      it "should set the timeout" do
        expect(shell_cmd.timeout).to eql(timeout)
      end

      it "should add environment settings to the default" do
        expect(shell_cmd.environment).to eql({ "RUBY_OPTS" => "-w" })
      end

      context "when setting custom environments" do
        context "when setting the :env option" do
          let(:options) { { env: environment } }

          it "should also set the environment" do
            expect(shell_cmd.environment).to eql({ "RUBY_OPTS" => "-w" })
          end
        end

        context "when setting environments with symbols" do
          let(:options) { { environment: { SYMBOL: "cymbal" } } }

          it "should also set the enviroment" do
            expect(shell_cmd.environment).to eql({ "SYMBOL" => "cymbal" })
          end
        end

        context "when :environment is set to nil" do
          let(:options) { { environment: nil } }

          it "should not set any environment" do
            expect(shell_cmd.environment).to eq({})
          end
        end

        context "when :env is set to nil" do
          let(:options) { { env: nil } }

          it "should not set any environment" do
            expect(shell_cmd.environment).to eql({})
          end
        end
      end

      it "should set valid exit codes" do
        expect(shell_cmd.valid_exit_codes).to eql(valid_exit_codes)
      end

      it "should set the live stream" do
        expect(shell_cmd.live_stream).to eql(stream)
      end

      it "should set the input" do
        expect(shell_cmd.input).to eql(input)
      end

      it "should set the cgroup" do
        expect(shell_cmd.cgroup).to eql(cgroup)
      end

      context "with an invalid option" do
        let(:options) { { frab: :job } }
        let(:invalid_option_exception) { Mixlib::ShellOut::InvalidCommandOption }
        let(:exception_message) { "option ':frab' is not a valid option for Mixlib::ShellOut" }

        it "should raise InvalidCommandOPtion" do
          expect { shell_cmd }.to raise_error(invalid_option_exception, exception_message)
        end
      end
    end

    context "with array of command and args" do
      let(:cmd) { [ "ruby", "-e", %q{'puts "hello"'} ] }

      context "without options" do
        let(:options) { nil }

        it "should set the command to the array of command and args" do
          expect(shell_cmd.command).to eql(cmd)
        end
      end

      context "with options" do
        let(:options) { { cwd: "/tmp", user: "nobody", password: "something" } }

        it "should set the command to the array of command and args" do
          expect(shell_cmd.command).to eql(cmd)
        end

        it "should evaluate the options" do
          expect(shell_cmd.cwd).to eql("/tmp")
          expect(shell_cmd.user).to eql("nobody")
          expect(shell_cmd.password).to eql("something")
        end
      end
    end
  end

  context "when executing the command" do
    let(:dir) { Dir.mktmpdir }
    let(:dump_file) { "#{dir}/out.txt" }
    let(:dump_file_content) { stdout; IO.read(dump_file) }

    context "with a current working directory" do
      subject { File.expand_path(chomped_stdout) }
      let(:fully_qualified_cwd) { File.expand_path(cwd) }
      let(:options) { { cwd: } }

      context "when running under Unix", :unix_only do
        # Use /bin for tests only if it is not a symlink. Some
        # distributions (e.g. Fedora) symlink it to /usr/bin
        let(:cwd) { File.symlink?("/bin") ? "/tmp" : "/bin" }
        let(:cmd) { "pwd" }

        it "should chdir to the working directory" do
          is_expected.to eql(fully_qualified_cwd)
        end
      end

      context "when running under Windows", :windows_only do
        let(:cwd) { Dir.tmpdir }
        let(:cmd) { "echo %cd%" }

        it "should chdir to the working directory" do
          is_expected.to eql(fully_qualified_cwd)
        end
      end
    end

    context "when handling locale" do
      before do
        @original_lc_all = ENV["LC_ALL"]
        ENV["LC_ALL"] = "en_US.UTF-8"
      end
      after do
        ENV["LC_ALL"] = @original_lc_all
      end

      subject { stripped_stdout }
      let(:cmd) { ECHO_LC_ALL }
      let(:options) { { environment: { "LC_ALL" => locale } } }

      context "without specifying environment" do
        let(:options) { nil }
        it "should no longer use the C locale by default" do
          is_expected.to eql("en_US.UTF-8")
        end
      end

      context "with locale" do
        let(:locale) { "es" }

        it "should use the requested locale" do
          is_expected.to eql(locale)
        end
      end

      context "with LC_ALL set to nil" do
        let(:locale) { nil }

        context "when running under Unix", :unix_only do
          it "should unset the process's locale" do
            is_expected.to eql("")
          end
        end

        context "when running under Windows", :windows_only do
          it "should unset process's locale" do
            is_expected.to eql("%LC_ALL%")
          end
        end
      end
    end

    context "when running under Windows", :windows_only do
      let(:cmd) { "%windir%/system32/whoami.exe" }
      let(:running_user) { shell_cmd.run_command.stdout.strip.downcase }

      context "when no user is set" do
        # Need to adjust the username and domain if running as local system
        # to match how whoami returns the information

        it "should run as current user" do
          if ENV["USERNAME"] == "#{ENV["COMPUTERNAME"]}$"
            expected_user = "nt authority\\system"
          else
            expected_user = "#{ENV["USERDOMAIN"].downcase}\\#{ENV["USERNAME"].downcase}"
          end
          expect(running_user).to eql(expected_user)
        end
      end

      context "when user is specified" do
        before do
          expect(system("net user #{user} #{password} /add")).to eq(true)
        end

        after do
          expect(system("net user #{user} /delete")).to eq(true)
        end

        let(:user) { "testuser" }
        let(:password) { "testpass1!" }
        let(:options) { { user:, password: } }

        it "should run as specified user" do
          expect(running_user).to eql("#{ENV["COMPUTERNAME"].downcase}\\#{user}")
        end

        context "when an alternate user is passed" do
          let(:env_list) { ["ALLUSERSPROFILE=C:\\ProgramData", "TEMP=C:\\Windows\\TEMP", "TMP=C:\\Windows\\TEMP", "USERDOMAIN=WIN-G06ENRTTKF9", "USERNAME=testuser", "USERPROFILE=C:\\Users\\Default", "windir=C:\\Windows"] }
          let(:current_env) { ["ALLUSERSPROFILE=C:\\ProgramData", "TEMP=C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\2", "TMP=C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\2", "USER=Administrator", "USERDOMAIN=WIN-G06ENRTTKF9", "USERDOMAIN_ROAMINGPROFILE=WIN-G06ENRTTKF9", "USERNAME=Administrator", "USERPROFILE=C:\\Users\\Administrator", "windir=C:\\Windows"] }
          let(:merged_env) { ["ALLUSERSPROFILE=C:\\ProgramData", "TEMP=C:\\Windows\\TEMP", "TMP=C:\\Windows\\TEMP", "USER=Administrator", "USERDOMAIN=WIN-G06ENRTTKF9", "USERDOMAIN_ROAMINGPROFILE=WIN-G06ENRTTKF9", "USERNAME=testuser", "USERPROFILE=C:\\Users\\Default", "windir=C:\\Windows"] }
          let(:converted) { { "ALLUSERSPROFILE" => "C:\\ProgramData", "TEMP" => "C:\\Windows\\TEMP", "TMP" => "C:\\Windows\\TEMP", "USERDOMAIN" => "WIN-G06ENRTTKF9", "USERNAME" => "testuser", "USERPROFILE" => "C:\\Users\\Default", "windir" => "C:\\Windows" } }

          it "merge environment variables" do
            expect(Process.merge_env_variables(env_list, current_env)).to eql(merged_env)
          end

          it "Convert an array to a hash" do
            expect(Process.environment_list_to_hash(env_list)).to eql(converted)
          end
        end

        context "when :elevated => true" do
          context "when user and password are passed" do
            let(:options) { { user:, password:, elevated: true } }

            it "raises permission related error" do
              expect { running_user }.to raise_error(/the user has not been granted the requested logon type at this computer/)
            end
          end

          context "when user and password are not passed" do
            let(:options) { { elevated: true } }

            it "raises error" do
              expect { running_user }.to raise_error("`elevated` option should be passed only with `username` and `password`.")
            end
          end
        end
      end
    end

    context "with a live stream" do
      let(:stream) { StringIO.new }
      let(:ruby_code) { '$stdout.puts "hello"; $stderr.puts "world"' }
      let(:options) { { live_stream: stream } }

      it "should copy the child's stdout to the live stream" do
        shell_cmd.run_command
        expect(stream.string).to include("hello#{LINE_ENDING}")
      end

      context "with default live stderr" do
        it "should copy the child's stderr to the live stream" do
          shell_cmd.run_command
          expect(stream.string).to include("world#{LINE_ENDING}")
        end
      end

      context "without live stderr" do
        it "should not copy the child's stderr to the live stream" do
          shell_cmd.live_stderr = nil
          shell_cmd.run_command
          expect(stream.string).not_to include("world#{LINE_ENDING}")
        end
      end

      context "with a separate live stderr" do
        let(:stderr_stream) { StringIO.new }

        it "should not copy the child's stderr to the live stream" do
          shell_cmd.live_stderr = stderr_stream
          shell_cmd.run_command
          expect(stream.string).not_to include("world#{LINE_ENDING}")
        end

        it "should copy the child's stderr to the live stderr stream" do
          shell_cmd.live_stderr = stderr_stream
          shell_cmd.run_command
          expect(stderr_stream.string).to include("world#{LINE_ENDING}")
        end
      end
    end

    context "with an input" do
      subject { stdout }

      let(:input) { "hello" }
      let(:ruby_code) { "STDIN.sync = true; STDOUT.sync = true; puts gets" }
      let(:options) { { input: } }

      it "should copy the input to the child's stdin" do
        is_expected.to eql("hello#{LINE_ENDING}")
      end
    end

    context "when running different types of command" do
      let(:script) { open_file.tap(&write_file).tap(&:close).tap(&make_executable) }
      let(:file_name) { "#{dir}/Setup Script.cmd" }
      let(:script_name) { "\"#{script.path}\"" }

      let(:open_file) { File.open(file_name, "w") }
      let(:write_file) { lambda { |f| f.write(script_content) } }
      let(:make_executable) { lambda { |f| File.chmod(0755, f.path) } }

      context "with spaces in the path" do
        subject { chomped_stdout }
        let(:cmd) { script_name }

        context "when running under Unix", :unix_only do
          let(:script_content) { "echo blah" }

          it "should execute" do
            is_expected.to eql("blah")
          end
        end

        context "when running under Windows", :windows_only do
          let(:cmd) { "#{script_name} #{argument}" }
          let(:script_content) { "@echo %1" }
          let(:argument) { rand(10000).to_s }

          it "should execute" do
            is_expected.to eql(argument)
          end

          context "with multiple quotes in the command and args" do
            context "when using a batch file" do
              let(:argument) { "\"Random #{rand(10000)}\"" }

              it "should execute" do
                is_expected.to eql(argument)
              end
            end

            context "when not using a batch file" do
              let(:cmd) { "#{executable_file_name} -command #{script_content}" }
              let(:executable_file_name) { "\"#{dir}/Powershell Parser.exe\"".tap(&make_executable!) }
              let(:make_executable!) { lambda { |filename| Mixlib::ShellOut.new("copy \"c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe\" #{filename}").run_command } }
              let(:script_content) { "Write-Host \"#{expected_output}\"" }
              let(:expected_output) { "Random #{rand(10000)}" }

              it "should execute" do
                is_expected.to eql(expected_output)
              end
            end
          end
        end
      end

      context "with lots of long arguments" do
        subject { chomped_stdout }

        # This number was chosen because it seems to be an actual maximum
        # in Windows--somewhere around 6-7K of command line
        let(:echotext) { 10000.upto(11340).map(&:to_s).join(" ") }
        let(:cmd) { "echo #{echotext}" }

        it "should execute" do
          is_expected.to eql(echotext)
        end
      end

      context "with special characters" do
        subject { stdout }

        let(:special_characters) { "<>&|&&||;" }
        let(:ruby_code) { "print \"#{special_characters}\"" }

        it "should execute" do
          is_expected.to eql(special_characters)
        end
      end

      context "with backslashes" do
        subject { stdout }
        let(:backslashes) { %q{\\"\\\\} }
        let(:cmd) { ruby_eval.call("print \"#{backslashes}\"") }

        it "should execute" do
          is_expected.to eql("\"\\")
        end
      end

      context "with pipes" do
        let(:input_script) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" }
        let(:output_script) { "print STDIN.read.length" }
        let(:cmd) { ruby_eval.call(input_script) + " | " + ruby_eval.call(output_script) }

        it "should execute" do
          expect(stdout).to eql("4")
        end

        it "should handle stderr" do
          expect(stderr).to eql("false")
        end
      end

      context "with stdout and stderr file pipes" do
        let(:code) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" }
        let(:cmd) { ruby_eval.call(code) + " > #{dump_file}" }

        it "should execute" do
          expect(stdout).to eql("")
        end

        it "should handle stderr" do
          expect(stderr).to eql("false")
        end

        it "should write to file pipe" do
          expect(dump_file_content).to eql("true")
        end
      end

      context "with stdin file pipe" do
        let(:code) { "STDIN.sync = true; STDOUT.sync = true; STDERR.sync = true; print gets; STDERR.print false" }
        let(:cmd) { ruby_eval.call(code) + " < #{dump_file_path}" }
        let(:file_content) { "Random content #{rand(100000)}" }

        let(:dump_file_path) { dump_file.path }
        let(:dump_file) { open_file.tap(&write_file).tap(&:close) }
        let(:file_name) { "#{dir}/input" }

        let(:open_file) { File.open(file_name, "w") }
        let(:write_file) { lambda { |f| f.write(file_content) } }

        it "should execute" do
          expect(stdout).to eql(file_content)
        end

        it "should handle stderr" do
          expect(stderr).to eql("false")
        end
      end

      context "with stdout and stderr file pipes" do
        let(:code) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" }
        let(:cmd) { ruby_eval.call(code) + " > #{dump_file} 2>&1" }

        it "should execute" do
          expect(stdout).to eql("")
        end

        it "should write to file pipe" do
          expect(dump_file_content).to eql("truefalse")
        end
      end

      context "with &&" do
        subject { stdout }
        let(:cmd) { ruby_eval.call('print "foo"') + " && " + ruby_eval.call('print "bar"') }

        it "should execute" do
          is_expected.to eql("foobar")
        end
      end

      context "with ||" do
        let(:cmd) { ruby_eval.call('print "foo"; exit 1') + " || " + ruby_eval.call('print "bar"') }

        it "should execute" do
          expect(stdout).to eql("foobar")
        end

        it "should exit with code 0" do
          expect(exit_status).to eql(0)
        end
      end
    end

    context "when handling process exit codes" do
      let(:cmd) { ruby_eval.call("exit #{exit_code}") }

      context "with normal exit status" do
        let(:exit_code) { 0 }

        it "should not raise error" do
          expect { executed_cmd.error! }.not_to raise_error
        end

        it "should set the exit status of the command" do
          expect(exit_status).to eql(exit_code)
        end
      end

      context "with nonzero exit status" do
        let(:exit_code) { 2 }
        let(:exception_message_format) { Regexp.escape(executed_cmd.format_for_exception) }

        it "should raise ShellCommandFailed" do
          expect { executed_cmd.error! }.to raise_error(Mixlib::ShellOut::ShellCommandFailed)
        end

        it "includes output with exceptions from #error!" do

          executed_cmd.error!
        rescue Mixlib::ShellOut::ShellCommandFailed => e
          expect(e.message).to match(exception_message_format)

        end

        it "should set the exit status of the command" do
          expect(exit_status).to eql(exit_code)
        end
      end

      context "with valid exit codes" do
        let(:cmd) { ruby_eval.call("exit #{exit_code}" ) }
        let(:options) { { returns: valid_exit_codes } }

        context "when exiting with valid code" do
          let(:valid_exit_codes) { 42 }
          let(:exit_code) { 42 }

          it "should not raise error" do
            expect { executed_cmd.error! }.not_to raise_error
          end

          it "should set the exit status of the command" do
            expect(exit_status).to eql(exit_code)
          end
        end

        context "when exiting with invalid code" do
          let(:valid_exit_codes) { [ 0, 1, 42 ] }
          let(:exit_code) { 2 }

          it "should raise ShellCommandFailed" do
            expect { executed_cmd.error! }.to raise_error(Mixlib::ShellOut::ShellCommandFailed)
          end

          it "should set the exit status of the command" do
            expect(exit_status).to eql(exit_code)
          end

          context "with input data" do
            let(:options) { { returns: valid_exit_codes, input: } }
            let(:input) { "Random data #{rand(1000000)}" }

            it "should raise ShellCommandFailed" do
              expect { executed_cmd.error! }.to raise_error(Mixlib::ShellOut::ShellCommandFailed)
            end

            it "should set the exit status of the command" do
              expect(exit_status).to eql(exit_code)
            end
          end
        end

        context "when exiting with invalid code 0" do
          let(:valid_exit_codes) { 42 }
          let(:exit_code) { 0 }

          it "should raise ShellCommandFailed" do
            expect { executed_cmd.error! }.to raise_error(Mixlib::ShellOut::ShellCommandFailed)
          end

          it "should set the exit status of the command" do
            expect(exit_status).to eql(exit_code)
          end
        end
      end

      describe "#invalid!" do
        let(:exit_code) { 0 }

        it "should raise ShellCommandFailed" do
          expect { executed_cmd.invalid!("I expected this to exit 42, not 0") }.to raise_error(Mixlib::ShellOut::ShellCommandFailed)
        end
      end

      describe "#error?" do
        context "when exiting with invalid code" do
          let(:exit_code) { 2 }

          it "should return true" do
            expect(executed_cmd.error?).to be_truthy
          end
        end

        context "when exiting with valid code" do
          let(:exit_code) { 0 }

          it "should return false" do
            expect(executed_cmd.error?).to be_falsey
          end
        end
      end
    end

    context "when handling the subprocess" do
      context "with STDOUT and STDERR" do
        let(:ruby_code) { "STDERR.puts :hello; STDOUT.puts :world" }

        # We could separate this into two examples, but we want to make
        # sure that stderr and stdout gets collected without stepping
        # on each other.
        it "should collect all of STDOUT and STDERR" do
          expect(stderr).to eql("hello#{LINE_ENDING}")
          expect(stdout).to eql("world#{LINE_ENDING}")
        end
      end

      context "with forking subprocess that does not close stdout and stderr" do
        let(:ruby_code) { "exit if fork; 10.times { sleep 1 }" }

        it "should not hang" do
          expect do
            Timeout.timeout(2) do
              executed_cmd
            end
          end.not_to raise_error
        end
      end

      context "when running a command that doesn't exist", :unix_only do

        let(:cmd) { "/bin/this-is-not-a-real-command" }

        def shell_out_cmd
          Mixlib::ShellOut.new(cmd)
        end

        it "reaps zombie processes after exec fails [OHAI-455]" do
          # NOTE: depending on ulimit settings, GC, etc., before the OHAI-455 patch,
          # ohai could also exhaust the available file descriptors when creating this
          # many zombie processes. A regression _could_ cause Errno::EMFILE but this
          # probably won't be consistent on different environments.
          created_procs = 0
          100.times do

            shell_out_cmd.run_command
          rescue Errno::ENOENT
            created_procs += 1

          end
          expect(created_procs).to eq(100)
          reaped_procs = 0
          begin
            loop { Process.wait(-1); reaped_procs += 1 }
          rescue Errno::ECHILD
          end
          expect(reaped_procs).to eq(0)
        end
      end

      context "with open files for parent process" do
        before do
          @test_file = Tempfile.new("fd_test")
          @test_file.write("hello")
          @test_file.flush
        end

        after do
          @test_file&.close
        end

        let(:ruby_code) { "fd = File.for_fd(#{@test_file.to_i}) rescue nil; if fd; fd.seek(0); puts fd.read(5); end" }

        it "should not see file descriptors of the parent" do
          # The reason this test goes through the effor of writing out
          # a file and checking the contents along side the presence of
          # a file descriptor is because on Windows, we're seeing that
          # a there is a file descriptor present, but it's not the same
          # file. That means that if we just check for the presence of
          # a file descriptor, the test would fail as that slot would
          # have something.
          #
          # See https://github.com/chef/mixlib-shellout/pull/103
          #
          expect(stdout.chomp).not_to eql("hello")
        end
      end

      context "when the child process dies immediately" do
        let(:cmd) { [ "exit" ] }

        it "handles ESRCH from getpgid of a zombie", :unix_only do
          allow(Process).to receive(:setsid) { exit!(4) }

          # we used to have race conditions if the child exited and zombied
          # quickly which would cause an exception.  we no longer call getpgrp()
          # after setsid()/setpgrp() though so this race condition should no
          # longer exist.  still test 5 times for it though.
          5.times do
            s = Mixlib::ShellOut.new(cmd)
            s.run_command # should not raise Errno::ESRCH (or anything else)
          end

        end

      end

      context "with subprocess that takes longer than timeout" do
        let(:options) { { timeout: 1 } }

        context "on windows", :windows_only do
          let(:cmd) do
            'cmd /c powershell -c "sleep 10"'
          end

          it "should raise CommandTimeout" do
            Timeout.timeout(5) do
              expect { executed_cmd }.to raise_error(Mixlib::ShellOut::CommandTimeout)
            end
          end

          context "and child processes should be killed" do
            it "kills the child processes" do
              expect(shell_cmd).to receive(:kill_process) do |instance|
                expect(instance.wmi_ole_object.Name).to match(/powershell.exe/)
                Process.kill(:KILL, instance.wmi_ole_object.processid)
              end
              expect { executed_cmd }.to raise_error(Mixlib::ShellOut::CommandTimeout)
            end
          end
        end

        context "on unix", :unix_only do
          def ruby_wo_shell(code)
            parts = %w{ruby}
            parts << "-e"
            parts << code
          end

          let(:cmd) do
            ruby_wo_shell(<<-CODE)
              STDOUT.sync = true
              trap(:TERM) { puts "got term"; exit!(123) }
              sleep 10
            CODE
          end

          it "should raise CommandTimeout" do
            expect { executed_cmd }.to raise_error(Mixlib::ShellOut::CommandTimeout)
          end

          it "should ask the process nicely to exit" do
            # note: let blocks don't correctly memoize if an exception is raised,
            # so can't use executed_cmd
            expect { shell_cmd.run_command }.to raise_error(Mixlib::ShellOut::CommandTimeout)
            expect(shell_cmd.stdout).to include("got term")
            expect(shell_cmd.exitstatus).to eq(123)
          end

          context "and the child is unresponsive" do
            let(:cmd) do
              ruby_wo_shell(<<-CODE)
                STDOUT.sync = true
                trap(:TERM) { puts "nanana cant hear you" }
                sleep 10
              CODE
            end

            it "should KILL the wayward child" do
              # note: let blocks don't correctly memoize if an exception is raised,
              # so can't use executed_cmd
              expect { shell_cmd.run_command }.to raise_error(Mixlib::ShellOut::CommandTimeout)
              expect(shell_cmd.stdout).to include("nanana cant hear you")
              expect(shell_cmd.status.termsig).to eq(9)
            end

            context "and a logger is configured" do
              let(:log_output) { StringIO.new }
              let(:logger) { Logger.new(log_output) }
              let(:options) { { timeout: 1, logger: } }

              it "should log messages about killing the child process" do
                # note: let blocks don't correctly memoize if an exception is raised,
                # so can't use executed_cmd
                expect { shell_cmd.run_command }.to raise_error(Mixlib::ShellOut::CommandTimeout)
                expect(shell_cmd.stdout).to include("nanana cant hear you")
                expect(shell_cmd.status.termsig).to eq(9)

                expect(log_output.string).to include("Command exceeded allowed execution time, sending TERM")
                expect(log_output.string).to include("Command exceeded allowed execution time, sending KILL")
              end

            end
          end

          context "and the child process forks grandchildren" do
            let(:cmd) do
              ruby_wo_shell(<<-CODE)
                STDOUT.sync = true
                trap(:TERM) { print "got term in child\n"; exit!(123) }
                fork do
                  trap(:TERM) { print "got term in grandchild\n"; exit!(142) }
                  sleep 10
                end
                sleep 10
              CODE
            end

            it "should TERM the wayward child and grandchild" do
              # note: let blocks don't correctly memoize if an exception is raised,
              # so can't use executed_cmd
              expect { shell_cmd.run_command }.to raise_error(Mixlib::ShellOut::CommandTimeout)
              expect(shell_cmd.stdout).to include("got term in child")
              expect(shell_cmd.stdout).to include("got term in grandchild")
            end

          end
          context "and the child process forks grandchildren that don't respond to TERM" do
            let(:cmd) do
              ruby_wo_shell(<<-CODE)
                STDOUT.sync = true

                trap(:TERM) { print "got term in child\n"; exit!(123) }
                fork do
                  trap(:TERM) { print "got term in grandchild\n" }
                  sleep 10
                end
                sleep 10
              CODE
            end

            it "should TERM the wayward child and grandchild, then KILL whoever is left" do
              # note: let blocks don't correctly memoize if an exception is raised,
              # so can't use executed_cmd
              expect { shell_cmd.run_command }.to raise_error(Mixlib::ShellOut::CommandTimeout)

              begin

                # A little janky. We get the process group id out of the command
                # object, then try to kill a process in it to make sure none
                # exists. Trusting the system under test like this isn't great but
                # it's difficult to test otherwise.
                child_pgid = shell_cmd.send(:child_pgid)
                initial_process_listing = `ps -j`

                expect(shell_cmd.stdout).to include("got term in child")
                expect(shell_cmd.stdout).to include("got term in grandchild")

                kill_return_val = Process.kill(:INT, child_pgid) # should raise ESRCH
                # AIX - kill returns code > 0 for error, where as other platforms return -1. Ruby code signal.c treats < 0 as error and raises exception and hence fails on AIX. So we check the return code for assertions since ruby wont raise an error here.

                if kill_return_val == 0
                  # Debug the failure:
                  puts "child pgid=#{child_pgid.inspect}"
                  Process.wait
                  puts "collected process: #{$?.inspect}"
                  puts "initial process listing:\n#{initial_process_listing}"
                  puts "current process listing:"
                  puts `ps -j`
                  raise "Failed to kill all expected processes"
                end
              rescue Errno::ESRCH
                # this is what we want
              end
            end

          end

        end
      end

      context "with subprocess that exceeds buffersize" do
        let(:ruby_code) { 'print("X" * 16 * 1024); print("." * 1024)' }

        it "should still reads all of the output" do
          expect(stdout).to match(/X{16384}\.{1024}/)
        end
      end

      context "with subprocess that returns nothing" do
        let(:ruby_code) { "exit 0" }

        it "should return an empty string for stdout" do
          expect(stdout).to eql("")
        end

        it "should return an empty string for stderr" do
          expect(stderr).to eql("")
        end
      end

      context "with subprocess that closes stdin and continues writing to stdout" do
        let(:ruby_code) { "STDIN.close; sleep 0.5; STDOUT.puts :win" }
        let(:options) { { input: "Random data #{rand(100000)}" } }

        it "should not hang or lose output" do
          expect(stdout).to eql("win#{LINE_ENDING}")
        end
      end

      context "with subprocess that closes stdout and continues writing to stderr" do
        let(:ruby_code) { "STDOUT.close; sleep 0.5; STDERR.puts :win" }

        it "should not hang or lose output" do
          expect(stderr).to eql("win#{LINE_ENDING}")
        end
      end

      context "with subprocess that closes stderr and continues writing to stdout" do
        let(:ruby_code) { "STDERR.close; sleep 0.5; STDOUT.puts :win" }

        it "should not hang or lose output" do
          expect(stdout).to eql("win#{LINE_ENDING}")
        end
      end

      # Regression test:
      #
      # We need to ensure that stderr is removed from the list of file
      # descriptors that we attempt to select() on in the case that:
      #
      # a) STDOUT closes first
      # b) STDERR closes
      # c) The program does not exit for some time after (b) occurs.
      #
      # Otherwise, we will attempt to read from the closed STDOUT pipe over and
      # over again and generate lots of garbage, which will not be collected
      # since we have to turn GC off to avoid segv.
      context "with subprocess that closes STDOUT before closing STDERR" do
        let(:ruby_code) { %q{STDOUT.puts "F" * 4096; STDOUT.close; sleep 0.1; STDERR.puts "foo"; STDERR.close; sleep 0.1; exit} }
        let(:unclosed_pipes) { executed_cmd.send(:open_pipes) }

        it "should not hang" do
          expect(stdout).not_to be_empty
        end

        it "should close all pipes", :unix_only do
          expect(unclosed_pipes).to be_empty
        end
      end

      context "with subprocess reading lots of data from stdin" do
        subject { stdout.to_i }
        let(:ruby_code) { "STDOUT.print gets.size" }
        let(:options) { { input: } }
        let(:input) { "f" * 20_000 }
        let(:input_size) { input.size }

        it "should not hang" do
          is_expected.to eql(input_size)
        end
      end

      context "with subprocess writing lots of data to both stdout and stderr" do
        let(:expected_output_with) { lambda { |chr| (chr * 20_000) + LINE_ENDING.to_s + (chr * 20_000) + LINE_ENDING.to_s } }

        context "when writing to STDOUT first" do
          let(:ruby_code) { %q{puts "f" * 20_000; STDERR.puts "u" * 20_000; puts "f" * 20_000; STDERR.puts "u" * 20_000} }

          it "should not deadlock" do
            expect(stdout).to eql(expected_output_with.call("f"))
            expect(stderr).to eql(expected_output_with.call("u"))
          end
        end

        context "when writing to STDERR first" do
          let(:ruby_code) { %q{STDERR.puts "u" * 20_000; puts "f" * 20_000; STDERR.puts "u" * 20_000; puts "f" * 20_000} }

          it "should not deadlock" do
            expect(stdout).to eql(expected_output_with.call("f"))
            expect(stderr).to eql(expected_output_with.call("u"))
          end
        end
      end

      context "with subprocess piping lots of data through stdin, stdout, and stderr" do
        let(:multiplier) { 20 }
        let(:expected_output_with) { lambda { |chr| (chr * multiplier) + (chr * multiplier) } }

        # Use regex to work across Ruby versions
        let(:ruby_code) { "STDOUT.sync = STDERR.sync = true; while(input = gets) do ( input =~ /^f/ ? STDOUT : STDERR ).print input.chomp; end" }

        let(:options) { { input: } }

        context "when writing to STDOUT first" do
          let(:input) { [ "f" * multiplier, "u" * multiplier, "f" * multiplier, "u" * multiplier ].join(LINE_ENDING) }

          it "should not deadlock" do
            expect(stdout).to eql(expected_output_with.call("f"))
            expect(stderr).to eql(expected_output_with.call("u"))
          end
        end

        context "when writing to STDERR first" do
          let(:input) { [ "u" * multiplier, "f" * multiplier, "u" * multiplier, "f" * multiplier ].join(LINE_ENDING) }

          it "should not deadlock" do
            expect(stdout).to eql(expected_output_with.call("f"))
            expect(stderr).to eql(expected_output_with.call("u"))
          end
        end
      end

      context "when subprocess closes prematurely", :unix_only do
        context "with input data" do
          let(:ruby_code) { "bad_ruby { [ } ]" }
          let(:options) { { input: } }
          # https://github.com/chef/mixlib-shellout/issues/204
          let(:repeats) { 20_000 * (Etc.sysconf(Etc::SC_PAGESIZE) / 4096) }
          let(:input) { [ "f" * repeats, "u" * repeats, "f" * repeats, "u" * repeats ].join(LINE_ENDING) }

          # Should the exception be handled?
          it "should raise error" do
            expect { executed_cmd }.to raise_error(Errno::EPIPE)
          end
        end
      end

      context "when subprocess writes, pauses, then continues writing" do
        subject { stdout }
        let(:ruby_code) { %q{puts "before"; sleep 0.5; puts "after"} }

        it "should not hang or lose output" do
          is_expected.to eql("before#{LINE_ENDING}after#{LINE_ENDING}")
        end
      end

      context "when subprocess pauses before writing" do
        subject { stdout }
        let(:ruby_code) { 'sleep 0.5; puts "missed_the_bus"' }

        it "should not hang or lose output" do
          is_expected.to eql("missed_the_bus#{LINE_ENDING}")
        end
      end

      context "when subprocess pauses before reading from stdin" do
        subject { stdout.to_i }
        let(:ruby_code) { "sleep 0.5; print gets.size " }
        let(:input) { "c" * 1024 }
        let(:input_size) { input.size }
        let(:options) { { input: } }

        it "should not hang or lose output" do
          is_expected.to eql(input_size)
        end
      end

      context "when execution fails" do
        let(:cmd) { "fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu" }

        context "when running under Unix", :unix_only do
          it "should recover the error message" do
            expect { executed_cmd }.to raise_error(Errno::ENOENT)
          end

          context "with input" do
            let(:options) { { input: } }
            let(:input) { "Random input #{rand(1000000)}" }

            it "should recover the error message" do
              expect { executed_cmd }.to raise_error(Errno::ENOENT)
            end
          end
        end

        skip "when running under Windows", :windows_only
      end

      context "without input data" do
        context "with subprocess that expects stdin" do
          let(:ruby_code) { %q{print STDIN.eof?.to_s} }

          # If we don't have anything to send to the subprocess, we need to close
          # stdin so that the subprocess won't wait for input.
          it "should close stdin" do
            expect(stdout).to eql("true")
          end
        end
      end
    end

    describe "#format_for_exception" do
      let(:ruby_code) { %q{STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"} }
      let(:exception_output) { executed_cmd.format_for_exception.split("\n") }
      let(:expected_output) do
        [
        "---- Begin output of #{cmd} ----",
        %q{STDOUT: msg_in_stdout},
        %q{STDERR: msg_in_stderr},
        "---- End output of #{cmd} ----",
        "Ran #{cmd} returned 0",
      ]
      end

      it "should format exception messages" do
        exception_output.each_with_index do |output_line, i|
          expect(output_line).to eql(expected_output[i])
        end
      end
    end
  end

  context "when running under *nix", :requires_root, :unix_only do
    let(:cmd) { "whoami" }
    let(:running_user) { shell_cmd.run_command.stdout.chomp }

    context "when no user is set" do
      it "should run as current user" do
        expect(running_user).to eql(Etc.getpwuid.name)
      end
    end

    context "when user is specified" do
      let(:user) { "nobody" }

      let(:options) { { user: } }

      it "should run as specified user" do
        expect(running_user).to eql(user.to_s)
      end
    end
  end

  context "when running on a cgroup", :linux_only do
    let(:cmd) { "cat /proc/self/cgroup | cut -c 4-" }
    let(:options) { { cgroup: } }
    let(:cgroupv2_supported) { File.read("/proc/mounts").match(%r{^cgroup2 /sys/fs/cgroup}) }

    context "when cgroup exists" do
      let(:cgroup) { "#{File.read("/proc/self/cgroup")[%r{(/.*)$}, 1]}" }
      let(:running_cgroup) { shell_cmd.run_command.stdout.chomp }
      it "should run the process under that cgroup" do
        if cgroupv2_supported
          expect(running_cgroup).to eql(cgroup.to_s)
        end
      end
    end

    context "when cgroup does not exist" do
      let(:cgroup) { "#{File.read("/proc/self/cgroup")[%r{(/.*)/[^/]+$}, 1]}/test" }
      let(:running_cgroup) { shell_cmd.run_command.stdout.chomp }
      it "should create the cgroup and run the process under it" do
        if cgroupv2_supported
          expect(running_cgroup).to eql(cgroup.to_s)
          Dir.rmdir("/sys/fs/cgroup/#{cgroup}")
        end
      end
    end
  end
end
