require "spec_helper"

module Omnibus
  describe Software do
    let(:project) do
      Project.new.evaluate do
        name "project"
        install_dir "/opt/project"
      end
    end

    let(:source) do
      {
        url: "http://example.com/",
        md5: "abcd1234",
      }
    end

    let(:internal_source) do
      {
        url: "http://internal.com/",
        md5: "efgh5678",
      }
    end

    let(:rel_path) { "software" }

    subject do
      local_source = source
      local_internal_source = internal_source
      local_rel_path = rel_path

      described_class.new(project).evaluate do
        name "software"
        default_version "1.2.3"

        source local_source
        internal_source local_internal_source
        relative_path local_rel_path
      end
    end

    it_behaves_like "a cleanroom getter", :project
    it_behaves_like "a cleanroom setter", :name, %{name 'libxml2'}
    it_behaves_like "a cleanroom setter", :description, %{description 'The XML magician'}
    it_behaves_like "a cleanroom setter", :maintainer, %{maintainer 'Captain Jack <sparrow@chef.io>'}
    it_behaves_like "a cleanroom setter", :dependency, %{dependency 'libxslt'}
    it_behaves_like "a cleanroom setter", :build_dependency, %{build_dependency 'libxslt-dev'}
    it_behaves_like "a cleanroom setter", :system_dependency, %{system_dependency 'libxslt'}
    it_behaves_like "a cleanroom setter", :source, %{source url: 'https://source.example.com'}
    it_behaves_like "a cleanroom setter", :default_version, %{default_version '1.2.3'}
    it_behaves_like "a cleanroom setter", :version, %{version '1.2.3'}
    it_behaves_like "a cleanroom setter", :license, %{license 'Apache 2.0'}
    it_behaves_like "a cleanroom setter", :license_file, %{license_file 'LICENSES/artistic.txt'}
    it_behaves_like "a cleanroom setter", :skip_transitive_dependency_licensing, %{skip_transitive_dependency_licensing true}
    it_behaves_like "a cleanroom setter", :dependency_licenses, %{dependency_licenses [{license: "MIT"}]}
    it_behaves_like "a cleanroom setter", :whitelist_file, %{whitelist_file '/opt/whatever'}
    it_behaves_like "a cleanroom setter", :relative_path, %{relative_path '/path/to/extracted'}
    it_behaves_like "a cleanroom setter", :build, %|build {}|
    it_behaves_like "a cleanroom getter", :project_dir
    it_behaves_like "a cleanroom getter", :build_dir
    it_behaves_like "a cleanroom getter", :install_dir
    it_behaves_like "a cleanroom getter", :with_standard_compiler_flags
    it_behaves_like "a cleanroom setter", :with_embedded_path, %|with_embedded_path({ 'foo' => 'bar' })|
    it_behaves_like "a cleanroom setter", :prepend_path, %|prepend_path({ 'foo' => 'bar' })|

    context "when a source_uri is present" do
      let(:uri) { URI.parse("http://example.com/foo.tar.gz") }
      before { allow(subject).to receive(:source_uri).and_return(uri) }

      it_behaves_like "a cleanroom getter", :project_file
    end

    context "when no license is present" do
      it "sets the defaults" do
        expect(subject.license).to eq ("Unspecified")
      end
    end

    describe "with_standard_compiler_flags helper" do
      context "on ubuntu" do
        before { stub_ohai(platform: "ubuntu", version: "16.04") }

        it "sets the defaults" do
          expect(subject.with_standard_compiler_flags).to eq(
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
        it "overrides LDFLAGS" do
          expect(subject.with_standard_compiler_flags("LDFLAGS" => "foo")).to eq(
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
        it "overrides CFLAGS" do
          expect(subject.with_standard_compiler_flags("CFLAGS" => "foo")).to eq(
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
        it "overrides CXXFLAGS" do
          expect(subject.with_standard_compiler_flags("CXXFLAGS" => "foo")).to eq(
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
        it "overrides CPPFLAGS" do
          expect(subject.with_standard_compiler_flags("CPPFLAGS" => "foo")).to eq(
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
        it "preserves anything else" do
          expect(subject.with_standard_compiler_flags("numberwang" => 4)).to eq(
            "numberwang"      => 4,
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
      end

      context "on solaris_11" do
        before do
          stub_ohai(platform: "solaris2", version: "5.11") do |data|
            # For some reason, this isn't set in Fauxhai
            data["platform"] = "solaris2"
          end
        end

        it "sets the defaults" do
          expect(subject.with_standard_compiler_flags).to eq(
            "CC"              => "gcc -m64 -static-libgcc",
            "CFLAGS"          => "-I/opt/project/embedded/include -O2",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O2",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O2",
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib -static-libgcc",
            "LD_OPTIONS"      => "-R/opt/project/embedded/lib",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end

        context "when loader mapping file is specified" do
          # Let the unit tests run on windows where auto-path translation occurs.
          let(:project_root) { File.join(tmp_path, "/root/project") }
          before do
            stub_ohai(platform: "solaris2", version: "5.11") do |data|
              # For some reason, this isn't set in Fauxhai
              data["platform"] = "solaris2"
            end
            Config.project_root(project_root)
            Config.solaris_linker_mapfile("files/mapfile/solaris")
            allow(File).to receive(:exist?).and_return(true)
          end

          it "sets LD_OPTIONS correctly" do
            expect(subject.with_standard_compiler_flags).to eq(
              "CC"              => "gcc -m64 -static-libgcc",
              "CFLAGS"          => "-I/opt/project/embedded/include -O2",
              "CPPFLAGS"        => "-I/opt/project/embedded/include -O2",
              "CXXFLAGS"        => "-I/opt/project/embedded/include -O2",
              "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib -static-libgcc",
              "LD_OPTIONS"      => "-R/opt/project/embedded/lib",
              "LD_RUN_PATH"     => "/opt/project/embedded/lib",
              "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
              "OMNIBUS_INSTALL_DIR" => "/opt/project"
            )
          end
        end
      end

      context "on mac_os_x" do
        before { stub_ohai(platform: "mac_os_x", version: "10.15") }

        it "sets the defaults" do
          expect(subject.with_standard_compiler_flags).to eq(
            "LDFLAGS"         => "-Wl,-rpath,/opt/project/embedded/lib -L/opt/project/embedded/lib",
            "CFLAGS"          => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"        => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
      end

      context "on aix" do
        before { stub_ohai(platform: "aix") }

        it "sets the defaults" do
          expect(subject.with_standard_compiler_flags).to eq(
            "CC"              => "xlc_r -q64",
            "CXX"             => "xlC_r -q64",
            "CFLAGS"          => "-q64 -I/opt/project/embedded/include -D_LARGE_FILES -O",
            "CXXFLAGS"        => "-q64 -I/opt/project/embedded/include -D_LARGE_FILES -O",
            "CPPFLAGS"        => "-q64 -I/opt/project/embedded/include -D_LARGE_FILES -O",
            "LDFLAGS"         => "-q64 -L/opt/project/embedded/lib -Wl,-blibpath:/opt/project/embedded/lib:/usr/lib:/lib",
            "LD"              => "ld -b64",
            "OBJECT_MODE"     => "64",
            "ARFLAGS"         => "-X64 cru",
            "LD_RUN_PATH"     => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
      end

      context "on freebsd" do
        before do
          stub_ohai(platform: "freebsd", version: "12.1")
        end

        it "sets the defaults" do
          expect(subject.with_standard_compiler_flags).to eq(
            "CC" => "clang",
            "CFLAGS" => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXXFLAGS"  => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CPPFLAGS"  => "-I/opt/project/embedded/include -O3 -D_FORTIFY_SOURCE=2 -fstack-protector",
            "CXX" => "clang++",
            "LDFLAGS" => "-L/opt/project/embedded/lib -Wl,-rpath,/opt/project/embedded/lib",
            "LD_RUN_PATH" => "/opt/project/embedded/lib",
            "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
            "OMNIBUS_INSTALL_DIR" => "/opt/project"
          )
        end
      end

      context "on windows" do
        let(:win_arch_i386) { true }

        before do
          stub_ohai(platform: "windows", version: "2012R2")
          allow(subject).to receive(:windows_arch_i386?).and_return(win_arch_i386)
        end

        context "in 32-bit mode" do
          it "sets the default" do
            expect(subject.with_standard_compiler_flags).to eq(
              "CFLAGS"          => "-I/opt/project/embedded/include -m32 -O3 -march=i686",
              "CXXFLAGS"        => "-I/opt/project/embedded/include -m32 -O3 -march=i686",
              "CPPFLAGS"        => "-I/opt/project/embedded/include -m32 -O3 -march=i686",
              "LDFLAGS"         => "-L/opt/project/embedded/lib -m32 -fno-lto",
              "LD_RUN_PATH"     => "/opt/project/embedded/lib",
              "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
              "OMNIBUS_INSTALL_DIR" => "/opt/project"
            )
          end
        end

        context "in 64-bit mode" do
          let(:win_arch_i386) { false }

          it "sets the default" do
            expect(subject.with_standard_compiler_flags).to eq(
              "CFLAGS"          => "-I/opt/project/embedded/include -m64 -O3 -march=x86-64",
              "CXXFLAGS"        => "-I/opt/project/embedded/include -m64 -O3 -march=x86-64",
              "CPPFLAGS"        => "-I/opt/project/embedded/include -m64 -O3 -march=x86-64",
              "LDFLAGS"         => "-L/opt/project/embedded/lib -m64 -fno-lto",
              "LD_RUN_PATH"     => "/opt/project/embedded/lib",
              "PKG_CONFIG_PATH" => "/opt/project/embedded/lib/pkgconfig",
              "OMNIBUS_INSTALL_DIR" => "/opt/project"
            )
          end
        end
      end

    end

    describe "path helpers" do

      before do
        stub_const("File::PATH_SEPARATOR", separator)
        stub_env("PATH", path)
        allow(project).to receive(:install_dir).and_return(install_dir)
      end

      let(:prepended_path) do
        ["#{install_dir}/bin", separator, "#{install_dir}/embedded/bin", separator, path].join
      end

      context "on *Nix" do
        let(:separator) { ":" }
        let(:path) { "/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin" }
        let(:install_dir) { "/opt/project" }

        it "prepends a path to PATH" do
          expect(subject.prepend_path("/foo/bar")).to eq(
            ["/foo/bar", separator, path].join
          )
        end

        it "prepends the embedded bin to PATH" do
          expect(subject.with_embedded_path).to eq(
            "PATH" => prepended_path
          )
        end

        it "with_embedded_path merges with a hash argument" do
          expect(subject.with_embedded_path("numberwang" => 4)).to eq(
            "numberwang" => 4,
            "PATH" => prepended_path
          )
        end

        it "prepends multiple paths to PATH" do
          expect(subject.prepend_path("/foo/bar", "/foo/baz")).to eq(
            ["/foo/bar", separator, "/foo/baz", separator, path].join
          )
        end
      end

      context "on Windows" do
        before do
          stub_ohai(platform: "windows", version: "2012R2")
        end

        let(:separator) { ";" }
        let(:path) { "c:/Ruby193/bin;c:/Windows/system32;c:/Windows;c:/Windows/System32/Wbem" }
        let(:install_dir) { "c:/opt/project" }
        let(:prepended_path_msys) do
          [ "#{install_dir}/bin", separator, "#{install_dir}/embedded/bin", separator,
            "#{install_dir}/embedded/msys/1.0/bin", separator, path].join
        end

        context "`Path` exists in the environment" do
          before do
            stub_env("Path", path)
            allow(ENV).to receive(:keys).and_return(%w{ Path PATH })
          end

          it "and raises an error when PATH is also set" do
            expect { subject.with_embedded_path }.to raise_error(
              RuntimeError,
              "The current omnibus environment has multiple PATH/Path variables."
            )
          end
        end

        context "`Path` does not exist in the environment" do
          before do
            allow(ENV).to receive(:keys).and_return(["PATH"])
          end

          it "returns a path key of `PATH`" do
            expect(subject.with_embedded_path).to eq(
              "PATH" => prepended_path
            )
          end
        end
      end
    end

    describe "#ohai" do
      before { stub_ohai(platform: "ubuntu", version: "16.04") }

      it "is a DSL method" do
        expect(subject).to have_exposed_method(:ohai)
      end

      it "delegates to the Ohai class" do
        expect(subject.ohai).to be(Ohai)
      end
    end

    describe "#manifest_entry" do
      let(:a_source) do
        { url: "http://example.com/",
          md5: "abcd1234" }
      end

      let(:manifest_entry) { Omnibus::ManifestEntry.new("software", { locked_version: "1.2.8", locked_source: a_source }) }
      let(:manifest) do
        m = Omnibus::Manifest.new
        m.add(:software, manifest_entry)
      end

      let(:project_with_manifest) do
        described_class.new(project, nil, manifest).evaluate do
          name "software"
          default_version "1.2.3"
          source url: "http://example.com/",
                 md5: "abcd1234"
        end
      end

      let(:project_without_manifest) do
        described_class.new(project, nil, nil).evaluate do
          name "software"
          default_version "1.2.3"
          source url: "http://example.com/",
                 md5: "abcd1234"
        end
      end

      let(:another_project) do
        described_class.new(project, nil, manifest).evaluate do
          name "ruroh"
        end
      end

      it "constructs a manifest entry if no manifest was provided" do
        expect(project_without_manifest.manifest_entry).to be_a Omnibus::ManifestEntry
        expect(project_without_manifest.manifest_entry.locked_version).to eq("1.2.3")
        expect(project_without_manifest.manifest_entry.locked_source).to eq(a_source)
      end

      it "constructs a manifest entry with a fully resolved version" do
        expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", a_source).and_return("1.2.8")
        expect(project_without_manifest.manifest_entry.locked_version).to eq("1.2.8")
      end

      it "returns the entry from the user-provided manifest if it was given one" do
        expect(project_with_manifest.manifest_entry).to eq(manifest_entry)
        expect(project_with_manifest.manifest_entry.locked_version).to eq("1.2.8")
        expect(project_with_manifest.manifest_entry.locked_source).to eq(a_source)
      end

      it "raises an error if it was given a manifest but can't find it's entry" do
        expect { another_project.manifest_entry }.to raise_error(Manifest::MissingManifestEntry)
      end
    end

    describe "#<=>" do
      let(:zlib)   { described_class.new(project).tap { |s| s.name("zlib") } }
      let(:erchef) { described_class.new(project).tap { |s| s.name("erchef") } }
      let(:bacon)  { described_class.new(project).tap { |s| s.name("bacon") } }

      it "compares projects by name" do
        list = [zlib, erchef, bacon]
        expect(list.sort.map(&:name)).to eq(%w{bacon erchef zlib})
      end
    end

    describe "#whitelist_file" do
      it "appends to the whitelist_files array" do
        expect(subject.whitelist_files.size).to eq(0)
        subject.whitelist_file(%r{foo/bar})
        expect(subject.whitelist_files.size).to eq(1)
      end

      it "converts Strings to Regexp instances" do
        subject.whitelist_file "foo/bar"
        expect(subject.whitelist_files.size).to eq(1)
        expect(subject.whitelist_files.first).to be_kind_of(Regexp)
      end
    end

    describe "#bin_dirs" do
      it "sets bin_dirs" do
        subject.bin_dirs ["my_bin_dir"]
        expect(subject.bin_dirs).to eq(["my_bin_dir"])
        expect(subject.bin_dirs).to be_kind_of(Array)
      end

      context "bin_dirs is not set" do
        it "returns default values" do
          expect(subject.bin_dirs).to eq(["/opt/project/bin", "/opt/project/embedded/bin"])
          expect(subject.bin_dirs).to be_kind_of(Array)
        end
      end
    end

    describe "#lib_dirs" do
      it "sets lib_dirs" do
        subject.lib_dirs ["my_lib_dir"]
        expect(subject.lib_dirs).to eq(["my_lib_dir"])
        expect(subject.lib_dirs).to be_kind_of(Array)
      end

      context "lib_dirs is not set" do
        it "returns default values" do
          expect(subject.lib_dirs).to eq(["/opt/project/embedded/lib"])
          expect(subject.lib_dirs).to be_kind_of(Array)
        end
      end
    end

    context "testing repo-level version overrides" do
      context "without overrides" do
        it "returns the original values" do
          expect(subject.version).to eq("1.2.3")
          expect(subject.source).to eq(url: "http://example.com/", md5: "abcd1234")
        end
      end

      context "with overrides for different software" do
        before { project.override(:chaos_monkey, version: "1.2.8") }

        it "does not change the software" do
          expect(subject.version).to eq("1.2.3")
        end
      end

      context "with overrides for this software" do
        context "version" do
          let(:version) { "2.0.0.pre" }
          before { project.override(:software, version: "2.0.0.pre") }

          it "returns the correct version" do
            expect(subject.version).to eq(version)
          end
        end

        context "source" do
          let(:source) { { url: "http://new.example.com", md5: "defg5678" } }
          before { project.override(:software, source: source) }

          it "returns the correct source" do
            expect(subject.source).to eq(source)
          end
        end
      end
    end

    context "when software source is a github spec" do
      let(:source) do
        {
          github: "chef/ohai",
        }
      end

      it "fetches from a fully expanded git path" do
        expect(subject.source).to eq(git: "https://github.com/chef/ohai.git")
        expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", { git: "https://github.com/chef/ohai.git" } ).and_return("1.2.8")
        subject.send(:fetcher)
      end

      context "and override source is a git spec" do
        before { project.override(:software, source: { git: "https://blah.com/git.git" }) }

        it "fetches from the override path" do
          expect(subject.source).to eq(git: "https://blah.com/git.git")
          expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", { git: "https://blah.com/git.git" } ).and_return("1.2.8")
          subject.send(:fetcher)
        end
      end

      context "and override source is a github spec" do
        before { project.override(:software, source: { github: "a/b" }) }

        it "fetches from the override path" do
          expect(subject.source).to eq(git: "https://github.com/a/b.git")
          expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", { git: "https://github.com/a/b.git" } ).and_return("1.2.8")
          subject.send(:fetcher)
        end
      end
    end

    context "when software source is a git spec" do
      let(:source) do
        {
          git: "https://blah.com/git.git",
        }
      end

      it "fetches from the git spec" do
        expect(subject.source).to eq(git: "https://blah.com/git.git")
        expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", { git: "https://blah.com/git.git" } ).and_return("1.2.8")
        subject.send(:fetcher)
      end

      context "and override source is a github spec" do
        before { project.override(:software, source: { github: "a/b" }) }

        it "fetches from the override path" do
          expect(subject.source).to eq(git: "https://github.com/a/b.git")
          expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", { git: "https://github.com/a/b.git" } ).and_return("1.2.8")
          subject.send(:fetcher)
        end
      end
    end

    context "when software internal_source is given" do
      before { Omnibus::Config.use_internal_sources(true) }

      it "sets the source with internal: true" do
        expect(subject.source).to eq(url: "http://internal.com/", md5: "efgh5678", internal: true)
      end
    end

    describe "#fetcher" do
      before do
        expect(Omnibus::Fetcher).to receive(:resolve_version).with("1.2.3", source).and_return("1.2.8")
      end

      context "when given a source url to an archive" do
        let(:source) do
          {
            url: "http://example.com/foo.tar.gz",
            md5: "abcd1234",
          }
        end

        context "when relative_path is the same as name" do
          let(:rel_path) { "software" }

          it "ignores back-compat and leaves fetch_dir alone" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end

          it "sets the fetcher project_dir to fetch_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software"))
          end
        end

        context "when relative_path is different from name" do
          let(:rel_path) { "foo" }

          it "ignores back-compat and leaves fetch_dir alone" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end

          it "sets the fetcher project_dir to fetch_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software"))
          end
        end
      end

      context "when given source url is not an archive" do
        let(:source) do
          {
            url: "http://example.com/foo.txt",
            md5: "abcd1234",
          }
        end

        context "when relative_path is the same as name" do
          let(:rel_path) { "software" }

          it "for back-compat, changes fetch_dir" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end

          it "sets the fetcher project_dir to project_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end
        end

        context "when relative_path is different from name" do
          let(:rel_path) { "foo" }

          it "ignores back-compat and leaves fetch_dir alone" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end

          it "sets the fetcher project_dir to project_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end
        end
      end

      context "when given source is a git repo" do
        let(:source) do
          {
            git: "http://example.com/my/git/repo",
          }
        end

        context "when relative_path is the same as name" do
          let(:rel_path) { "software" }

          it "for back-compat, changes fetch_dir" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end

          it "sets the fetcher project_dir to project_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end
        end

        context "when relative_path is different from name" do
          let(:rel_path) { "foo" }

          it "ignores back-compat and leaves fetch_dir alone" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end

          it "sets the fetcher project_dir to project_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end
        end
      end

      context "when given source is a local file path" do
        let(:source) do
          {
            file: "../foo.tar.gz",
          }
        end

        context "when relative_path is the same as name" do
          let(:rel_path) { "software" }

          it "for back-compat, changes fetch_dir" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end

          it "sets the fetcher project_dir to project_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software/software"))
          end
        end

        context "when relative_path is different from name" do
          let(:rel_path) { "foo" }

          it "ignores back-compat and leaves fetch_dir alone" do
            subject.send(:fetcher)
            expect(subject.project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end

          it "sets the fetcher project_dir to project_dir" do
            expect(subject.send(:fetcher).project_dir).to eq(File.expand_path("#{Config.source_dir}/software/foo"))
          end
        end
      end
    end

    describe "#canonicalize_source" do
      it 'canonicalize_source(github: "chef/chef") yields git: "https://github.com/chef/chef.git"' do
        expect(subject.send(:canonicalize_source, github: "chef/chef")).to eq(git: "https://github.com/chef/chef.git")
      end
      it 'canonicalize_source(github: "chef/chef", submodules: true) yields git: "https://github.com/chef/chef.git", submodules: true' do
        expect(subject.send(:canonicalize_source, github: "chef/chef", submodules: true)).to eq(git: "https://github.com/chef/chef.git", submodules: true)
      end
      it "canonicalize_source does not overwrite the original" do
        original = { github: "chef/chef", submodules: true }
        expect(subject.send(:canonicalize_source, original)).to eq(git: "https://github.com/chef/chef.git", submodules: true)
        expect(original).to eq(github: "chef/chef", submodules: true)
      end
    end

    describe "#shasum" do
      context "when a filepath is given" do
        let(:path) { "/software.rb" }
        let(:file) { double(File) }

        before { subject.instance_variable_set(:@filepath, path) }

        before do
          allow(File).to receive(:exist?)
            .with(path)
            .and_return(true)
          allow(File).to receive(:open)
            .with(path)
            .and_return(file)
        end

        it "returns the correct shasum" do
          expect(subject.shasum).to eq("69dcce6da5580abe1da581e3f09d81e13ac676c48790eb0aa44d0ca2f93a16de")
        end
      end

      context "when a filepath is not given" do
        before { subject.send(:remove_instance_variable, :@filepath) }

        it "returns the correct shasum" do
          expect(subject.shasum).to eq("acd88f56f17b7cbc146f351a9265b652bcf96d544821e7bc1e9663c80617276d")
        end
      end
    end
  end
end
