# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

import filecmp
import json
import os
import shutil

import jsonschema
import pytest

from llnl.util.filesystem import mkdirp, working_dir

import spack
import spack.binary_distribution
import spack.ci as ci
import spack.config
import spack.environment as ev
import spack.hash_types as ht
import spack.main
import spack.paths as spack_paths
import spack.repo as repo
import spack.util.gpg
import spack.util.spack_yaml as syaml
import spack.util.url as url_util
from spack.schema.buildcache_spec import schema as specfile_schema
from spack.schema.ci import schema as ci_schema
from spack.schema.database_index import schema as db_idx_schema
from spack.spec import Spec
from spack.util.pattern import Bunch

config_cmd = spack.main.SpackCommand("config")
ci_cmd = spack.main.SpackCommand("ci")
env_cmd = spack.main.SpackCommand("env")
mirror_cmd = spack.main.SpackCommand("mirror")
gpg_cmd = spack.main.SpackCommand("gpg")
install_cmd = spack.main.SpackCommand("install")
uninstall_cmd = spack.main.SpackCommand("uninstall")
buildcache_cmd = spack.main.SpackCommand("buildcache")

pytestmark = [pytest.mark.not_on_windows("does not run on windows"), pytest.mark.maybeslow]


@pytest.fixture()
def ci_base_environment(working_env, tmpdir):
    os.environ["CI_PROJECT_DIR"] = tmpdir.strpath


@pytest.fixture(scope="function")
def mock_git_repo(git, tmpdir):
    """Create a mock git repo with two commits, the last one creating
    a .gitlab-ci.yml"""

    repo_path = tmpdir.join("mockspackrepo").strpath
    mkdirp(repo_path)

    with working_dir(repo_path):
        git("init")

        with open("README.md", "w") as f:
            f.write("# Introduction")

        with open(".gitlab-ci.yml", "w") as f:
            f.write(
                """
testjob:
    script:
        - echo "success"
            """
            )

        git("config", "--local", "user.email", "testing@spack.io")
        git("config", "--local", "user.name", "Spack Testing")

        # initial commit with README
        git("add", "README.md")
        git("-c", "commit.gpgsign=false", "commit", "-m", "initial commit")

        # second commit, adding a .gitlab-ci.yml
        git("add", ".gitlab-ci.yml")
        git("-c", "commit.gpgsign=false", "commit", "-m", "add a .gitlab-ci.yml")

        yield repo_path


def test_specs_staging(config, tmpdir):
    """Make sure we achieve the best possible staging for the following
spec DAG::

        a
       /|
      c b
        |\
        e d
          |\
          f g

In this case, we would expect 'c', 'e', 'f', and 'g' to be in the first stage,
and then 'd', 'b', and 'a' to be put in the next three stages, respectively.

"""
    builder = repo.MockRepositoryBuilder(tmpdir)
    builder.add_package("g")
    builder.add_package("f")
    builder.add_package("e")
    builder.add_package("d", dependencies=[("f", None, None), ("g", None, None)])
    builder.add_package("c")
    builder.add_package("b", dependencies=[("d", None, None), ("e", None, None)])
    builder.add_package("a", dependencies=[("b", None, None), ("c", None, None)])

    with repo.use_repositories(builder.root):
        spec_a = Spec("a").concretized()

        spec_a_label = ci._spec_deps_key(spec_a)
        spec_b_label = ci._spec_deps_key(spec_a["b"])
        spec_c_label = ci._spec_deps_key(spec_a["c"])
        spec_d_label = ci._spec_deps_key(spec_a["d"])
        spec_e_label = ci._spec_deps_key(spec_a["e"])
        spec_f_label = ci._spec_deps_key(spec_a["f"])
        spec_g_label = ci._spec_deps_key(spec_a["g"])

        spec_labels, dependencies, stages = ci.stage_spec_jobs([spec_a])

        assert len(stages) == 4

        assert len(stages[0]) == 4
        assert spec_c_label in stages[0]
        assert spec_e_label in stages[0]
        assert spec_f_label in stages[0]
        assert spec_g_label in stages[0]

        assert len(stages[1]) == 1
        assert spec_d_label in stages[1]

        assert len(stages[2]) == 1
        assert spec_b_label in stages[2]

        assert len(stages[3]) == 1
        assert spec_a_label in stages[3]


def test_ci_generate_with_env(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    ci_base_environment,
    mock_binary_index,
):
    """Make sure we can get a .gitlab-ci.yml from an environment file
    which has the gitlab-ci, cdash, and mirrors sections."""
    mirror_url = "https://my.fake.mirror"
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  definitions:
    - old-gcc-pkgs:
      - archive-files
      - callpath
      # specify ^openblas-with-lapack to ensure that builtin.mock repo flake8
      # package (which can also provide lapack) is not chosen, as it violates
      # a package-level check which requires exactly one fetch strategy (this
      # is apparently not an issue for other tests that use it).
      - hypre@0.2.15 ^openblas-with-lapack
  specs:
    - matrix:
      - [$old-gcc-pkgs]
  mirrors:
    some-mirror: {0}
  ci:
    pipeline-gen:
    - submapping:
      - match:
          - arch=test-debian6-core2
        build-job:
          tags:
            - donotcare
          image: donotcare
      - match:
          - arch=test-debian6-m1
        build-job:
          tags:
            - donotcare
          image: donotcare
    - cleanup-job:
        image: donotcare
        tags: [donotcare]
    - reindex-job:
        script:: [hello, world]
        custom_attribute: custom!
  cdash:
    build-group: Not important
    url: https://my.fake.cdash
    project: Not used
    site: Nothing
""".format(
                mirror_url
            )
        )
    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)
            assert "workflow" in yaml_contents
            assert "rules" in yaml_contents["workflow"]
            assert yaml_contents["workflow"]["rules"] == [{"when": "always"}]

            assert "stages" in yaml_contents
            assert len(yaml_contents["stages"]) == 5
            assert yaml_contents["stages"][0] == "stage-0"
            assert yaml_contents["stages"][4] == "stage-rebuild-index"

            assert "rebuild-index" in yaml_contents
            rebuild_job = yaml_contents["rebuild-index"]
            expected = "spack buildcache update-index --keys {0}".format(mirror_url)
            assert rebuild_job["script"][0] == expected
            assert rebuild_job["custom_attribute"] == "custom!"

            assert "variables" in yaml_contents
            assert "SPACK_ARTIFACTS_ROOT" in yaml_contents["variables"]
            artifacts_root = yaml_contents["variables"]["SPACK_ARTIFACTS_ROOT"]
            assert artifacts_root == "jobs_scratch_dir"


def test_ci_generate_with_env_missing_section(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    ci_base_environment,
    mock_binary_index,
):
    """Make sure we get a reasonable message if we omit gitlab-ci section"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  mirrors:
    some-mirror: https://my.fake.mirror
"""
        )

    expect_out = "Environment does not have `ci` a configuration"

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")

        with ev.read("test"):
            output = ci_cmd("generate", fail_on_error=False, output=str)
            assert expect_out in output


def test_ci_generate_with_cdash_token(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    ci_base_environment,
    mock_binary_index,
):
    """Make sure we it doesn't break if we configure cdash"""
    os.environ.update({"SPACK_CDASH_AUTH_TOKEN": "notreallyatokenbutshouldnotmatter"})
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    enable-artifacts-buildcache: True
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
        build-job:
          tags:
            - donotcare
          image: donotcare
  cdash:
    build-group: Not important
    url: https://my.fake.cdash
    project: Not used
    site: Nothing
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")

        with ev.read("test"):
            copy_to_file = str(tmpdir.join("backup-ci.yml"))
            output = ci_cmd("generate", "--copy-to", copy_to_file, output=str)
            # That fake token should still have resulted in being unable to
            # register build group with cdash, but the workload should
            # still have been generated.
            expect = "Problem populating buildgroup"
            assert expect in output

            dir_contents = os.listdir(tmpdir.strpath)

            assert "backup-ci.yml" in dir_contents

            orig_file = str(tmpdir.join(".gitlab-ci.yml"))

            assert filecmp.cmp(orig_file, copy_to_file) is True


def test_ci_generate_with_custom_settings(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
    mock_binary_index,
):
    """Test use of user-provided scripts and attributes"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
        build-job:
          tags:
            - donotcare
          variables:
            ONE: plain-string-value
            TWO: ${INTERP_ON_BUILD}
          before_script:
            - mkdir /some/path
            - pushd /some/path
            - git clone ${SPACK_REPO}
            - cd spack
            - git checkout ${SPACK_REF}
            - popd
          script:
            - spack -d ci rebuild
          after_script:
            - rm -rf /some/path/spack
          custom_attribute: custom!
          artifacts:
            paths:
            - some/custom/artifact
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            monkeypatch.setattr(spack.main, "get_version", lambda: "0.15.3")
            monkeypatch.setattr(spack.main, "get_spack_commit", lambda: "big ol commit sha")
            ci_cmd("generate", "--output-file", outputfile)

            with open(outputfile) as f:
                contents = f.read()
                yaml_contents = syaml.load(contents)

                found_it = False

                global_vars = yaml_contents["variables"]
                assert global_vars["SPACK_VERSION"] == "0.15.3"
                assert global_vars["SPACK_CHECKOUT_VERSION"] == "big ol commit sha"

                for ci_key in yaml_contents.keys():
                    ci_obj = yaml_contents[ci_key]
                    if "archive-files" in ci_key:
                        # Ensure we have variables, possibly interpolated
                        var_d = ci_obj["variables"]
                        assert var_d["ONE"] == "plain-string-value"
                        assert var_d["TWO"] == "${INTERP_ON_BUILD}"

                        # Ensure we have scripts verbatim
                        assert ci_obj["before_script"] == [
                            "mkdir /some/path",
                            "pushd /some/path",
                            "git clone ${SPACK_REPO}",
                            "cd spack",
                            "git checkout ${SPACK_REF}",
                            "popd",
                        ]
                        assert ci_obj["script"][1].startswith("cd ")
                        ci_obj["script"][1] = "cd ENV"
                        assert ci_obj["script"] == [
                            "spack -d ci rebuild",
                            "cd ENV",
                            "spack env activate --without-view .",
                            "spack ci rebuild",
                        ]
                        assert ci_obj["after_script"] == ["rm -rf /some/path/spack"]

                        # Ensure we have the custom attributes
                        assert "some/custom/artifact" in ci_obj["artifacts"]["paths"]
                        assert ci_obj["custom_attribute"] == "custom!"

                        found_it = True

            assert found_it


def test_ci_generate_pkg_with_deps(
    tmpdir, working_env, mutable_mock_env_path, install_mockery, mock_packages, ci_base_environment
):
    """Test pipeline generation for a package w/ dependencies"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - flatten-deps
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    enable-artifacts-buildcache: True
    pipeline-gen:
    - submapping:
      - match:
          - flatten-deps
        build-job:
          tags:
            - donotcare
      - match:
          - dependency-install
        build-job:
          tags:
            - donotcare
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)
            found = []
            for ci_key in yaml_contents.keys():
                ci_obj = yaml_contents[ci_key]
                if "dependency-install" in ci_key:
                    assert "stage" in ci_obj
                    assert ci_obj["stage"] == "stage-0"
                    found.append("dependency-install")
                if "flatten-deps" in ci_key:
                    assert "stage" in ci_obj
                    assert ci_obj["stage"] == "stage-1"
                    found.append("flatten-deps")

            assert "flatten-deps" in found
            assert "dependency-install" in found


def test_ci_generate_for_pr_pipeline(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
):
    """Test that PR pipelines do not include a final stage job for
    rebuilding the mirror index, even if that job is specifically
    configured"""
    os.environ.update(
        {"SPACK_PIPELINE_TYPE": "spack_pull_request", "SPACK_PR_BRANCH": "fake-test-branch"}
    )
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - flatten-deps
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    enable-artifacts-buildcache: True
    pipeline-gen:
    - submapping:
      - match:
          - flatten-deps
        build-job:
          tags:
            - donotcare
      - match:
          - dependency-install
        build-job:
          tags:
            - donotcare
    - cleanup-job:
        image: donotcare
        tags: [donotcare]
    rebuild-index: False
"""
        )

    monkeypatch.setattr(spack.ci, "SHARED_PR_MIRROR_URL", "https://fake.shared.pr.mirror")

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)

            assert "rebuild-index" not in yaml_contents

            assert "variables" in yaml_contents
            pipeline_vars = yaml_contents["variables"]
            assert "SPACK_PIPELINE_TYPE" in pipeline_vars
            assert pipeline_vars["SPACK_PIPELINE_TYPE"] == "spack_pull_request"


def test_ci_generate_with_external_pkg(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
):
    """Make sure we do not generate jobs for external pkgs"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
    - externaltest
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
          - externaltest
        build-job:
          tags:
            - donotcare
          image: donotcare
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            yaml_contents = syaml.load(f)

        # Check that the "externaltool" package was not erroneously staged
        assert not any("externaltool" in key for key in yaml_contents)


def test_ci_rebuild_missing_config(tmpdir, working_env, mutable_mock_env_path):
    spack_yaml_contents = """
spack:
  specs:
    - archive-files
"""

    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        env_cmd("activate", "--without-view", "--sh", "test")
        out = ci_cmd("rebuild", fail_on_error=False)
        assert "env containing ci" in out

        env_cmd("deactivate")


def _signing_key():
    signing_key_dir = spack_paths.mock_gpg_keys_path
    signing_key_path = os.path.join(signing_key_dir, "package-signing-key")
    with open(signing_key_path) as fd:
        key = fd.read()
    return key


def create_rebuild_env(tmpdir, pkg_name, broken_tests=False):
    working_dir = tmpdir.join("working_dir")

    log_dir = os.path.join(working_dir.strpath, "logs")
    repro_dir = os.path.join(working_dir.strpath, "repro")
    test_dir = os.path.join(working_dir.strpath, "test")
    env_dir = working_dir.join("concrete_env")

    mirror_dir = working_dir.join("mirror")
    mirror_url = url_util.path_to_file_url(mirror_dir.strpath)

    broken_specs_path = os.path.join(working_dir.strpath, "naughty-list")
    broken_specs_url = url_util.path_to_file_url(broken_specs_path)
    temp_storage_url = "file:///path/to/per/pipeline/storage"

    broken_tests_packages = [pkg_name] if broken_tests else []

    ci_job_url = "https://some.domain/group/project/-/jobs/42"
    ci_pipeline_url = "https://some.domain/group/project/-/pipelines/7"

    spack_yaml_contents = """
spack:
  definitions:
    - packages: [{0}]
  specs:
    - $packages
  mirrors:
    test-mirror: {1}
  ci:
    broken-specs-url: {2}
    broken-tests-packages: {3}
    temporary-storage-url-prefix: {4}
    pipeline-gen:
    - submapping:
      - match:
          - {0}
        build-job:
          tags:
            - donotcare
          image: donotcare
  cdash:
    build-group: Not important
    url: https://my.fake.cdash
    project: Not used
    site: Nothing
""".format(
        pkg_name, mirror_url, broken_specs_url, broken_tests_packages, temp_storage_url
    )

    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        with ev.read("test") as env:
            with env.write_transaction():
                env.concretize()
                env.write()

            if not os.path.exists(env_dir.strpath):
                os.makedirs(env_dir.strpath)

            shutil.copyfile(env.manifest_path, os.path.join(env_dir.strpath, "spack.yaml"))
            shutil.copyfile(env.lock_path, os.path.join(env_dir.strpath, "spack.lock"))

            root_spec_dag_hash = None

            for h, s in env.specs_by_hash.items():
                if s.name == pkg_name:
                    root_spec_dag_hash = h

            assert root_spec_dag_hash

    return Bunch(
        broken_spec_file=os.path.join(broken_specs_path, root_spec_dag_hash),
        ci_job_url=ci_job_url,
        ci_pipeline_url=ci_pipeline_url,
        env_dir=env_dir,
        log_dir=log_dir,
        mirror_dir=mirror_dir,
        mirror_url=mirror_url,
        repro_dir=repro_dir,
        root_spec_dag_hash=root_spec_dag_hash,
        test_dir=test_dir,
        working_dir=working_dir,
    )


def activate_rebuild_env(tmpdir, pkg_name, rebuild_env):
    env_cmd("activate", "--without-view", "--sh", "-d", ".")

    # Create environment variables as gitlab would do it
    os.environ.update(
        {
            "SPACK_ARTIFACTS_ROOT": rebuild_env.working_dir.strpath,
            "SPACK_JOB_LOG_DIR": rebuild_env.log_dir,
            "SPACK_JOB_REPRO_DIR": rebuild_env.repro_dir,
            "SPACK_JOB_TEST_DIR": rebuild_env.test_dir,
            "SPACK_LOCAL_MIRROR_DIR": rebuild_env.mirror_dir.strpath,
            "SPACK_CONCRETE_ENV_DIR": rebuild_env.env_dir.strpath,
            "CI_PIPELINE_ID": "7192",
            "SPACK_SIGNING_KEY": _signing_key(),
            "SPACK_JOB_SPEC_DAG_HASH": rebuild_env.root_spec_dag_hash,
            "SPACK_JOB_SPEC_PKG_NAME": pkg_name,
            "SPACK_COMPILER_ACTION": "NONE",
            "SPACK_CDASH_BUILD_NAME": pkg_name,
            "SPACK_REMOTE_MIRROR_URL": rebuild_env.mirror_url,
            "SPACK_PIPELINE_TYPE": "spack_protected_branch",
            "CI_JOB_URL": rebuild_env.ci_job_url,
            "CI_PIPELINE_URL": rebuild_env.ci_pipeline_url,
            "CI_PROJECT_DIR": tmpdir.join("ci-project").strpath,
        }
    )


@pytest.mark.parametrize("broken_tests", [True, False])
def test_ci_rebuild_mock_success(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery_mutable_config,
    mock_gnupghome,
    mock_stage,
    mock_fetch,
    mock_binary_index,
    monkeypatch,
    broken_tests,
):
    pkg_name = "archive-files"
    rebuild_env = create_rebuild_env(tmpdir, pkg_name, broken_tests)

    monkeypatch.setattr(spack.cmd.ci, "SPACK_COMMAND", "echo")
    monkeypatch.setattr(spack.cmd.ci, "MAKE_COMMAND", "echo")

    with rebuild_env.env_dir.as_cwd():
        activate_rebuild_env(tmpdir, pkg_name, rebuild_env)

        out = ci_cmd("rebuild", "--tests", fail_on_error=False)

        # We didn"t really run the build so build output file(s) are missing
        assert "Unable to copy files" in out
        assert "No such file or directory" in out

        if broken_tests:
            # We generate a skipped tests report in this case
            assert "Unable to run stand-alone tests" in out
        else:
            # No installation means no package to test and no test log to copy
            assert "Cannot copy test logs" in out


@pytest.mark.skip(reason="fails intermittently and covered by gitlab ci")
def test_ci_rebuild(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery_mutable_config,
    mock_packages,
    monkeypatch,
    mock_gnupghome,
    mock_fetch,
    ci_base_environment,
    mock_binary_index,
):
    pkg_name = "archive-files"
    rebuild_env = create_rebuild_env(tmpdir, pkg_name)

    # Create job directories to be removed before processing (for coverage)
    os.makedirs(rebuild_env.log_dir)
    os.makedirs(rebuild_env.repro_dir)
    os.makedirs(rebuild_env.test_dir)

    with rebuild_env.env_dir.as_cwd():
        activate_rebuild_env(tmpdir, pkg_name, rebuild_env)

        ci_cmd("rebuild", "--tests", fail_on_error=False)

    monkeypatch.setattr(spack.cmd.ci, "SPACK_COMMAND", "notcommand")
    monkeypatch.setattr(spack.cmd.ci, "MAKE_COMMAND", "notcommand")
    monkeypatch.setattr(spack.cmd.ci, "INSTALL_FAIL_CODE", 127)

    with rebuild_env.env_dir.as_cwd():
        activate_rebuild_env(tmpdir, pkg_name, rebuild_env)

        expected_repro_files = [
            "install.sh",
            "root.json",
            "archive-files.json",
            "spack.yaml",
            "spack.lock",
        ]
        repro_files = os.listdir(rebuild_env.repro_dir)
        assert all([f in repro_files for f in expected_repro_files])

        install_script_path = os.path.join(rebuild_env.repro_dir, "install.sh")
        install_line = None
        with open(install_script_path) as fd:
            for line in fd:
                if line.startswith('"notcommand"'):
                    install_line = line

        assert install_line

        def mystrip(s):
            return s.strip('"').rstrip("\n").rstrip('"')

        install_parts = [mystrip(s) for s in install_line.split(" ")]

        assert "--keep-stage" in install_parts
        assert "--no-check-signature" not in install_parts
        assert "-f" in install_parts
        flag_index = install_parts.index("-f")
        assert "archive-files.json" in install_parts[flag_index + 1]

        with open(rebuild_env.broken_spec_file) as fd:
            broken_spec_content = fd.read()
            assert rebuild_env.ci_job_url in broken_spec_content
            assert rebuild_env.ci_pipeline_url in broken_spec_content

        # Ensure also produce CDash output for skipped (or notrun) tests
        test_files = os.listdir(rebuild_env.test_dir)
        with open(os.path.join(rebuild_env.test_dir, test_files[0]), "r") as f:
            have = False
            for line in f:
                if "notrun" in line:
                    have = True
                    break
            assert have

        env_cmd("deactivate")


def test_ci_require_signing(
    tmpdir, working_env, mutable_mock_env_path, mock_gnupghome, ci_base_environment
):
    spack_yaml_contents = """
spack:
 specs:
   - archive-files
 mirrors:
   test-mirror: file:///no-such-mirror
 ci:
   pipeline-gen:
   - submapping:
     - match:
         - archive-files
       build-job:
         tags:
           - donotcare
         image: donotcare
"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("activate", "--without-view", "--sh", "-d", ".")

        # Run without the variable to make sure we don't accidentally require signing
        output = ci_cmd("rebuild", output=str, fail_on_error=False)
        assert "spack must have exactly one signing key" not in output

        # Now run with the variable to make sure it works
        os.environ.update({"SPACK_REQUIRE_SIGNING": "True"})
        output = ci_cmd("rebuild", output=str, fail_on_error=False)

        assert "spack must have exactly one signing key" in output


def test_ci_nothing_to_rebuild(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    mock_fetch,
    ci_base_environment,
    mock_binary_index,
):
    working_dir = tmpdir.join("working_dir")

    mirror_dir = working_dir.join("mirror")
    mirror_url = "file://{0}".format(mirror_dir.strpath)

    spack_yaml_contents = """
spack:
 definitions:
   - packages: [archive-files]
 specs:
   - $packages
 mirrors:
   test-mirror: {0}
 ci:
   enable-artifacts-buildcache: True
   pipeline-gen:
   - submapping:
     - match:
         - archive-files
       build-job:
         tags:
           - donotcare
         image: donotcare
""".format(
        mirror_url
    )

    install_cmd("archive-files")
    buildcache_cmd("push", "-f", "-u", mirror_url, "archive-files")

    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        with ev.read("test") as env:
            env.concretize()
            root_spec_dag_hash = None

            for h, s in env.specs_by_hash.items():
                if s.name == "archive-files":
                    root_spec_dag_hash = h

            # Create environment variables as gitlab would do it
            os.environ.update(
                {
                    "SPACK_ARTIFACTS_ROOT": working_dir.strpath,
                    "SPACK_JOB_LOG_DIR": "log_dir",
                    "SPACK_JOB_REPRO_DIR": "repro_dir",
                    "SPACK_JOB_TEST_DIR": "test_dir",
                    "SPACK_LOCAL_MIRROR_DIR": mirror_dir.strpath,
                    "SPACK_CONCRETE_ENV_DIR": tmpdir.strpath,
                    "SPACK_JOB_SPEC_DAG_HASH": root_spec_dag_hash,
                    "SPACK_JOB_SPEC_PKG_NAME": "archive-files",
                    "SPACK_COMPILER_ACTION": "NONE",
                    "SPACK_REMOTE_MIRROR_URL": mirror_url,
                }
            )

            def fake_dl_method(spec, *args, **kwargs):
                print("fake download buildcache {0}".format(spec.name))

            monkeypatch.setattr(spack.binary_distribution, "download_single_spec", fake_dl_method)

            ci_out = ci_cmd("rebuild", output=str)

            assert "No need to rebuild archive-files" in ci_out
            assert "fake download buildcache archive-files" in ci_out

            env_cmd("deactivate")


def test_ci_generate_mirror_override(
    tmpdir,
    mutable_mock_env_path,
    install_mockery_mutable_config,
    mock_packages,
    mock_fetch,
    mock_stage,
    mock_binary_index,
    ci_base_environment,
):
    """Ensure that protected pipelines using --buildcache-destination do not
    skip building specs that are not in the override mirror when they are
    found in the main mirror."""
    os.environ.update({"SPACK_PIPELINE_TYPE": "spack_protected_branch"})

    working_dir = tmpdir.join("working_dir")

    mirror_dir = working_dir.join("mirror")
    mirror_url = "file://{0}".format(mirror_dir.strpath)

    spack_yaml_contents = """
spack:
 definitions:
   - packages: [patchelf]
 specs:
   - $packages
 mirrors:
   test-mirror: {0}
 ci:
   pipeline-gen:
   - submapping:
     - match:
         - patchelf
       build-job:
         tags:
           - donotcare
         image: donotcare
   - cleanup-job:
       tags:
         - nonbuildtag
       image: basicimage
""".format(
        mirror_url
    )

    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        first_ci_yaml = str(tmpdir.join(".gitlab-ci-1.yml"))
        second_ci_yaml = str(tmpdir.join(".gitlab-ci-2.yml"))
        with ev.read("test"):
            install_cmd()
            buildcache_cmd("push", "-u", mirror_url, "patchelf")
            buildcache_cmd("update-index", mirror_url, output=str)

            # This generate should not trigger a rebuild of patchelf, since it's in
            # the main mirror referenced in the environment.
            ci_cmd("generate", "--check-index-only", "--output-file", first_ci_yaml)

            # Because we used a mirror override (--buildcache-destination) on a
            # spack protected pipeline, we expect to only look in the override
            # mirror for the spec, and thus the patchelf job should be generated in
            # this pipeline
            ci_cmd(
                "generate",
                "--check-index-only",
                "--output-file",
                second_ci_yaml,
                "--buildcache-destination",
                "file:///mirror/not/exist",
            )

        with open(first_ci_yaml) as fd1:
            first_yaml = fd1.read()
            assert "no-specs-to-rebuild" in first_yaml

        with open(second_ci_yaml) as fd2:
            second_yaml = fd2.read()
            assert "no-specs-to-rebuild" not in second_yaml


@pytest.mark.disable_clean_stage_check
def test_push_mirror_contents(
    tmpdir,
    mutable_mock_env_path,
    install_mockery_mutable_config,
    mock_packages,
    mock_fetch,
    mock_stage,
    mock_gnupghome,
    ci_base_environment,
    mock_binary_index,
):
    working_dir = tmpdir.join("working_dir")

    mirror_dir = working_dir.join("mirror")
    mirror_url = url_util.path_to_file_url(mirror_dir.strpath)

    ci.import_signing_key(_signing_key())

    with tmpdir.as_cwd():
        with open("spack.yaml", "w") as f:
            f.write(
                f"""\
spack:
 definitions:
   - packages: [patchelf]
 specs:
   - $packages
 mirrors:
   test-mirror: {mirror_url}
 ci:
   enable-artifacts-buildcache: True
   pipeline-gen:
   - submapping:
     - match:
         - patchelf
       build-job:
         tags:
           - donotcare
         image: donotcare
   - cleanup-job:
       tags:
         - nonbuildtag
       image: basicimage
   - any-job:
       tags:
         - nonbuildtag
       image: basicimage
       custom_attribute: custom!
"""
            )
        env_cmd("create", "test", "./spack.yaml")
        with ev.read("test"):
            concrete_spec = Spec("patchelf").concretized()
            spec_json = concrete_spec.to_json(hash=ht.dag_hash)
            json_path = str(tmpdir.join("spec.json"))
            with open(json_path, "w") as ypfd:
                ypfd.write(spec_json)

            install_cmd("--add", "--keep-stage", json_path)

            for s in concrete_spec.traverse():
                ci.push_mirror_contents(s, mirror_url, True)

            buildcache_path = os.path.join(mirror_dir.strpath, "build_cache")

            # Now test the --prune-dag (default) option of spack ci generate
            mirror_cmd("add", "test-ci", mirror_url)

            outputfile_pruned = str(tmpdir.join("pruned_pipeline.yml"))
            ci_cmd("generate", "--output-file", outputfile_pruned)

            with open(outputfile_pruned) as f:
                contents = f.read()
                yaml_contents = syaml.load(contents)
                # Make sure there are no other spec jobs or rebuild-index
                assert set(yaml_contents.keys()) == {"no-specs-to-rebuild", "workflow"}

                the_elt = yaml_contents["no-specs-to-rebuild"]
                assert "tags" in the_elt
                assert "nonbuildtag" in the_elt["tags"]
                assert "image" in the_elt
                assert the_elt["image"] == "basicimage"
                assert the_elt["custom_attribute"] == "custom!"

                assert "rules" in yaml_contents["workflow"]
                assert yaml_contents["workflow"]["rules"] == [{"when": "always"}]

            outputfile_not_pruned = str(tmpdir.join("unpruned_pipeline.yml"))
            ci_cmd("generate", "--no-prune-dag", "--output-file", outputfile_not_pruned)

            # Test the --no-prune-dag option of spack ci generate
            with open(outputfile_not_pruned) as f:
                contents = f.read()
                yaml_contents = syaml.load(contents)

                found_spec_job = False

                for ci_key in yaml_contents.keys():
                    if "patchelf" in ci_key:
                        the_elt = yaml_contents[ci_key]
                        assert "variables" in the_elt
                        job_vars = the_elt["variables"]
                        assert "SPACK_SPEC_NEEDS_REBUILD" in job_vars
                        assert job_vars["SPACK_SPEC_NEEDS_REBUILD"] == "False"
                        assert the_elt["custom_attribute"] == "custom!"
                        found_spec_job = True

                assert found_spec_job

            mirror_cmd("rm", "test-ci")

            # Test generating buildcache index while we have bin mirror
            buildcache_cmd("update-index", mirror_url)
            index_path = os.path.join(buildcache_path, "index.json")
            with open(index_path) as idx_fd:
                index_object = json.load(idx_fd)
                jsonschema.validate(index_object, db_idx_schema)

            # Now that index is regenerated, validate "buildcache list" output
            buildcache_list_output = buildcache_cmd("list", output=str)
            assert "patchelf" in buildcache_list_output
            # Also test buildcache_spec schema
            bc_files_list = os.listdir(buildcache_path)
            for file_name in bc_files_list:
                if file_name.endswith(".spec.json.sig"):
                    spec_json_path = os.path.join(buildcache_path, file_name)
                    with open(spec_json_path) as json_fd:
                        json_object = Spec.extract_json_from_clearsig(json_fd.read())
                        jsonschema.validate(json_object, specfile_schema)

            logs_dir = working_dir.join("logs_dir")
            if not os.path.exists(logs_dir.strpath):
                os.makedirs(logs_dir.strpath)

            ci.copy_stage_logs_to_artifacts(concrete_spec, logs_dir.strpath)

            logs_dir_list = os.listdir(logs_dir.strpath)

            assert "spack-build-out.txt" in logs_dir_list

            # Also just make sure that if something goes wrong with the
            # stage logs copy, no exception is thrown
            ci.copy_stage_logs_to_artifacts(concrete_spec, None)
            ci.copy_stage_logs_to_artifacts(None, logs_dir.strpath)

            dl_dir = working_dir.join("download_dir")
            if not os.path.exists(dl_dir.strpath):
                os.makedirs(dl_dir.strpath)
            buildcache_cmd("download", "--spec-file", json_path, "--path", dl_dir.strpath)
            dl_dir_list = os.listdir(dl_dir.strpath)

            assert len(dl_dir_list) == 2


def test_push_mirror_contents_exceptions(monkeypatch, capsys):
    def failing_access(*args, **kwargs):
        raise Exception("Error: Access Denied")

    monkeypatch.setattr(spack.ci, "_push_mirror_contents", failing_access)

    # Input doesn't matter, as wwe are faking exceptional output
    url = "fakejunk"
    ci.push_mirror_contents(None, url, None)

    captured = capsys.readouterr()
    std_out = captured[0]
    expect_msg = "Permission problem writing to {0}".format(url)

    assert expect_msg in std_out


@pytest.mark.parametrize("match_behavior", ["first", "merge"])
@pytest.mark.parametrize("git_version", ["big ol commit sha", None])
def test_ci_generate_override_runner_attrs(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
    match_behavior,
    git_version,
):
    """Test that we get the behavior we want with respect to the provision
    of runner attributes like tags, variables, and scripts, both when we
    inherit them from the top level, as well as when we override one or
    more at the runner level"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - flatten-deps
    - a
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    pipeline-gen:
    - match_behavior: {0}
      submapping:
        - match:
            - flatten-deps
          build-job:
            tags:
              - specific-one
            variables:
              THREE: specificvarthree
        - match:
            - dependency-install
        - match:
            - a
          build-job:
            tags:
              - specific-a-2
        - match:
            - a
          build-job-remove:
            tags:
              - toplevel2
          build-job:
            tags:
              - specific-a
            variables:
              ONE: specificvarone
              TWO: specificvartwo
            before_script::
              - - custom pre step one
            script::
              - - custom main step
            after_script::
              - custom post step one
    - build-job:
        tags:
          - toplevel
          - toplevel2
        variables:
          ONE: toplevelvarone
          TWO: toplevelvartwo
        before_script:
          - - pre step one
            - pre step two
        script::
          - - main step
        after_script:
          - - post step one
    - cleanup-job:
        image: donotcare
        tags: [donotcare]
""".format(
                match_behavior
            )
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            monkeypatch.setattr(spack, "spack_version", "0.20.0.test0")
            monkeypatch.setattr(spack.main, "get_version", lambda: "0.20.0.test0 (blah)")
            monkeypatch.setattr(spack.main, "get_spack_commit", lambda: git_version)
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)

            assert "variables" in yaml_contents
            global_vars = yaml_contents["variables"]
            assert "SPACK_VERSION" in global_vars
            assert global_vars["SPACK_VERSION"] == "0.20.0.test0 (blah)"
            assert "SPACK_CHECKOUT_VERSION" in global_vars
            assert global_vars["SPACK_CHECKOUT_VERSION"] == git_version or "v0.20.0.test0"

            for ci_key in yaml_contents.keys():
                if ci_key.startswith("a"):
                    # Make sure a's attributes override variables, and all the
                    # scripts.  Also, make sure the 'toplevel' tag doesn't
                    # appear twice, but that a's specific extra tag does appear
                    the_elt = yaml_contents[ci_key]
                    assert the_elt["variables"]["ONE"] == "specificvarone"
                    assert the_elt["variables"]["TWO"] == "specificvartwo"
                    assert "THREE" not in the_elt["variables"]
                    assert len(the_elt["tags"]) == (2 if match_behavior == "first" else 3)
                    assert "specific-a" in the_elt["tags"]
                    if match_behavior == "merge":
                        assert "specific-a-2" in the_elt["tags"]
                    assert "toplevel" in the_elt["tags"]
                    assert "toplevel2" not in the_elt["tags"]
                    assert len(the_elt["before_script"]) == 1
                    assert the_elt["before_script"][0] == "custom pre step one"
                    assert len(the_elt["script"]) == 1
                    assert the_elt["script"][0] == "custom main step"
                    assert len(the_elt["after_script"]) == 1
                    assert the_elt["after_script"][0] == "custom post step one"
                if "dependency-install" in ci_key:
                    # Since the dependency-install match omits any
                    # runner-attributes, make sure it inherited all the
                    # top-level attributes.
                    the_elt = yaml_contents[ci_key]
                    assert the_elt["variables"]["ONE"] == "toplevelvarone"
                    assert the_elt["variables"]["TWO"] == "toplevelvartwo"
                    assert "THREE" not in the_elt["variables"]
                    assert len(the_elt["tags"]) == 2
                    assert "toplevel" in the_elt["tags"]
                    assert "toplevel2" in the_elt["tags"]
                    assert len(the_elt["before_script"]) == 2
                    assert the_elt["before_script"][0] == "pre step one"
                    assert the_elt["before_script"][1] == "pre step two"
                    assert len(the_elt["script"]) == 1
                    assert the_elt["script"][0] == "main step"
                    assert len(the_elt["after_script"]) == 1
                    assert the_elt["after_script"][0] == "post step one"
                if "flatten-deps" in ci_key:
                    # The flatten-deps match specifies that we keep the two
                    # top level variables, but add a third specifc one.  It
                    # also adds a custom tag which should be combined with
                    # the top-level tag.
                    the_elt = yaml_contents[ci_key]
                    assert the_elt["variables"]["ONE"] == "toplevelvarone"
                    assert the_elt["variables"]["TWO"] == "toplevelvartwo"
                    assert the_elt["variables"]["THREE"] == "specificvarthree"
                    assert len(the_elt["tags"]) == 3
                    assert "specific-one" in the_elt["tags"]
                    assert "toplevel" in the_elt["tags"]
                    assert "toplevel2" in the_elt["tags"]
                    assert len(the_elt["before_script"]) == 2
                    assert the_elt["before_script"][0] == "pre step one"
                    assert the_elt["before_script"][1] == "pre step two"
                    assert len(the_elt["script"]) == 1
                    assert the_elt["script"][0] == "main step"
                    assert len(the_elt["after_script"]) == 1
                    assert the_elt["after_script"][0] == "post step one"


def test_ci_generate_with_workarounds(
    tmpdir, mutable_mock_env_path, install_mockery, mock_packages, monkeypatch, ci_base_environment
):
    """Make sure the post-processing cli workarounds do what they should"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - callpath%gcc@=9.5
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    pipeline-gen:
    - submapping:
      - match: ['%gcc@9.5']
        build-job:
          tags:
            - donotcare
          image: donotcare
    enable-artifacts-buildcache: true
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile, "--dependencies")

            with open(outputfile) as f:
                contents = f.read()
                yaml_contents = syaml.load(contents)

                found_one = False
                non_rebuild_keys = ["workflow", "stages", "variables", "rebuild-index"]

                for ci_key in yaml_contents.keys():
                    if ci_key not in non_rebuild_keys:
                        found_one = True
                        job_obj = yaml_contents[ci_key]
                        assert "needs" not in job_obj
                        assert "dependencies" in job_obj

                assert found_one is True


@pytest.mark.disable_clean_stage_check
def test_ci_rebuild_index(
    tmpdir,
    working_env,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    mock_fetch,
    mock_stage,
):
    working_dir = tmpdir.join("working_dir")

    mirror_dir = working_dir.join("mirror")
    mirror_url = "file://{0}".format(mirror_dir.strpath)

    spack_yaml_contents = """
spack:
 specs:
   - callpath
 mirrors:
   test-mirror: {0}
 ci:
   pipeline-gen:
   - submapping:
     - match:
         - patchelf
       build-job:
         tags:
           - donotcare
         image: donotcare
""".format(
        mirror_url
    )

    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        with ev.read("test"):
            concrete_spec = Spec("callpath").concretized()
            spec_json = concrete_spec.to_json(hash=ht.dag_hash)
            json_path = str(tmpdir.join("spec.json"))
            with open(json_path, "w") as ypfd:
                ypfd.write(spec_json)

            install_cmd("--add", "--keep-stage", "-f", json_path)
            buildcache_cmd("push", "-u", "-f", mirror_url, "callpath")
            ci_cmd("rebuild-index")

            buildcache_path = os.path.join(mirror_dir.strpath, "build_cache")
            index_path = os.path.join(buildcache_path, "index.json")
            with open(index_path) as idx_fd:
                index_object = json.load(idx_fd)
                jsonschema.validate(index_object, db_idx_schema)


def test_ci_get_stack_changed(mock_git_repo, monkeypatch):
    """Test that we can detect the change to .gitlab-ci.yml in a
    mock spack git repo."""
    monkeypatch.setattr(spack.paths, "prefix", mock_git_repo)
    assert ci.get_stack_changed("/no/such/env/path") is True


def test_ci_generate_prune_untouched(
    tmpdir, mutable_mock_env_path, install_mockery, mock_packages, ci_base_environment, monkeypatch
):
    """Test pipeline generation with pruning works to eliminate
    specs that were not affected by a change"""
    os.environ.update({"SPACK_PRUNE_UNTOUCHED": "TRUE"})  # enables pruning of untouched specs
    mirror_url = "https://my.fake.mirror"
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
    - callpath
  mirrors:
    some-mirror: {0}
  ci:
    pipeline-gen:
    - build-job:
        tags:
          - donotcare
        image: donotcare
""".format(
                mirror_url
            )
        )

    # Dependency graph rooted at callpath
    # callpath -> dyninst -> libelf
    #                     -> libdwarf -> libelf
    #          -> mpich

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        def fake_compute_affected(r1=None, r2=None):
            return ["libdwarf"]

        def fake_stack_changed(env_path, rev1="HEAD^", rev2="HEAD"):
            return False

        env_hashes = {}

        with ev.read("test") as active_env:
            monkeypatch.setattr(ci, "compute_affected_packages", fake_compute_affected)
            monkeypatch.setattr(ci, "get_stack_changed", fake_stack_changed)

            active_env.concretize()

            for s in active_env.all_specs():
                env_hashes[s.name] = s.dag_hash()

            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            print(contents)
            yaml_contents = syaml.load(contents)

            generated_hashes = []

            for ci_key in yaml_contents.keys():
                if "variables" in yaml_contents[ci_key]:
                    generated_hashes.append(
                        yaml_contents[ci_key]["variables"]["SPACK_JOB_SPEC_DAG_HASH"]
                    )

            assert env_hashes["archive-files"] not in generated_hashes
            for spec_name in ["callpath", "dyninst", "mpich", "libdwarf", "libelf"]:
                assert env_hashes[spec_name] in generated_hashes


def test_ci_generate_prune_env_vars(
    tmpdir, mutable_mock_env_path, install_mockery, mock_packages, ci_base_environment, monkeypatch
):
    """Make sure environment variables controlling untouched spec
    pruning behave as expected."""
    os.environ.update({"SPACK_PRUNE_UNTOUCHED": "TRUE"})  # enables pruning of untouched specs
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - libelf
  gitlab-ci:
    mappings:
      - match:
          - arch=test-debian6-core2
        runner-attributes:
          tags:
            - donotcare
          image: donotcare
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")

        def fake_compute_affected(r1=None, r2=None):
            return ["libdwarf"]

        def fake_stack_changed(env_path, rev1="HEAD^", rev2="HEAD"):
            return False

        expected_depth_param = None

        def check_get_spec_filter_list(env, affected_pkgs, dependent_traverse_depth=None):
            assert dependent_traverse_depth == expected_depth_param
            return set()

        monkeypatch.setattr(ci, "compute_affected_packages", fake_compute_affected)
        monkeypatch.setattr(ci, "get_stack_changed", fake_stack_changed)
        monkeypatch.setattr(ci, "get_spec_filter_list", check_get_spec_filter_list)

        expectations = {"-1": -1, "0": 0, "True": None}

        for key, val in expectations.items():
            with ev.read("test"):
                os.environ.update({"SPACK_PRUNE_UNTOUCHED_DEPENDENT_DEPTH": key})
                expected_depth_param = val
                # Leaving out the mirror in the spack.yaml above means the
                # pipeline generation command will fail, pretty much immediately.
                # But for this test, we only care how the environment variables
                # for pruning are handled, the faster the better.  So allow the
                # spack command to fail.
                ci_cmd("generate", fail_on_error=False)


def test_ci_subcommands_without_mirror(
    tmpdir,
    mutable_mock_env_path,
    mock_packages,
    install_mockery,
    ci_base_environment,
    mock_binary_index,
):
    """Make sure we catch if there is not a mirror and report an error"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  ci:
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
        build-job:
          tags:
            - donotcare
          image: donotcare
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            # Check the 'generate' subcommand
            output = ci_cmd(
                "generate", "--output-file", outputfile, output=str, fail_on_error=False
            )
            ex = "spack ci generate requires an env containing a mirror"
            assert ex in output

            # Also check the 'rebuild-index' subcommand
            output = ci_cmd("rebuild-index", output=str, fail_on_error=False)
            ex = "spack ci rebuild-index requires an env containing a mirror"
            assert ex in output


def test_ensure_only_one_temporary_storage():
    """Make sure 'gitlab-ci' section of env does not allow specification of
    both 'enable-artifacts-buildcache' and 'temporary-storage-url-prefix'."""
    gitlab_ci_template = """
  ci:
    {0}
    pipeline-gen:
    - submapping:
      - match:
          - notcheckedhere
        build-job:
          tags:
            - donotcare
"""

    enable_artifacts = "enable-artifacts-buildcache: True"
    temp_storage = "temporary-storage-url-prefix: file:///temp/mirror"
    specify_both = """{0}
    {1}
""".format(
        enable_artifacts, temp_storage
    )
    specify_neither = ""

    # User can specify "enable-artifacts-buildcache" (boolean)
    yaml_obj = syaml.load(gitlab_ci_template.format(enable_artifacts))
    jsonschema.validate(yaml_obj, ci_schema)

    # User can also specify "temporary-storage-url-prefix" (string)
    yaml_obj = syaml.load(gitlab_ci_template.format(temp_storage))
    jsonschema.validate(yaml_obj, ci_schema)

    # However, specifying both should fail to validate
    yaml_obj = syaml.load(gitlab_ci_template.format(specify_both))
    with pytest.raises(jsonschema.ValidationError):
        jsonschema.validate(yaml_obj, ci_schema)

    # Specifying neither should be fine too, as neither of these properties
    # should be required
    yaml_obj = syaml.load(gitlab_ci_template.format(specify_neither))
    jsonschema.validate(yaml_obj, ci_schema)


def test_ci_generate_temp_storage_url(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
    mock_binary_index,
):
    """Verify correct behavior when using temporary-storage-url-prefix"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    temporary-storage-url-prefix: file:///work/temp/mirror
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
        build-job:
          tags:
            - donotcare
          image: donotcare
    - cleanup-job:
        custom_attribute: custom!
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

            with open(outputfile) as of:
                pipeline_doc = syaml.load(of.read())

                assert "cleanup" in pipeline_doc
                cleanup_job = pipeline_doc["cleanup"]

                assert cleanup_job["custom_attribute"] == "custom!"

                assert "script" in cleanup_job
                cleanup_task = cleanup_job["script"][0]

                assert cleanup_task.startswith("spack -d mirror destroy")

                assert "stages" in pipeline_doc
                stages = pipeline_doc["stages"]

                # Cleanup job should be 2nd to last, just before rebuild-index
                assert "stage" in cleanup_job
                assert cleanup_job["stage"] == stages[-2]


def test_ci_generate_read_broken_specs_url(
    tmpdir, mutable_mock_env_path, install_mockery, mock_packages, monkeypatch, ci_base_environment
):
    """Verify that `broken-specs-url` works as intended"""
    spec_a = Spec("a")
    spec_a.concretize()
    a_dag_hash = spec_a.dag_hash()

    spec_flattendeps = Spec("flatten-deps")
    spec_flattendeps.concretize()
    flattendeps_dag_hash = spec_flattendeps.dag_hash()

    broken_specs_url = "file://{0}".format(tmpdir.strpath)

    # Mark 'a' as broken (but not 'flatten-deps')
    broken_spec_a_url = "{0}/{1}".format(broken_specs_url, a_dag_hash)
    job_stack = "job_stack"
    a_job_url = "a_job_url"
    ci.write_broken_spec(
        broken_spec_a_url, spec_a.name, job_stack, a_job_url, "pipeline_url", spec_a.to_dict()
    )

    # Test that `spack ci generate` notices this broken spec and fails.
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - flatten-deps
    - a
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    broken-specs-url: "{0}"
    pipeline-gen:
    - submapping:
      - match:
          - a
          - flatten-deps
          - b
          - dependency-install
        build-job:
          tags:
            - donotcare
          image: donotcare
""".format(
                broken_specs_url
            )
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        with ev.read("test"):
            # Check output of the 'generate' subcommand
            output = ci_cmd("generate", output=str, fail_on_error=False)
            assert "known to be broken" in output

            expected = "{0}/{1} (in stack {2}) was reported broken here: {3}".format(
                spec_a.name, a_dag_hash[:7], job_stack, a_job_url
            )
            assert expected in output

            not_expected = "flatten-deps/{0} (in stack".format(flattendeps_dag_hash[:7])
            assert not_expected not in output


def test_ci_generate_external_signing_job(
    tmpdir, mutable_mock_env_path, install_mockery, mock_packages, monkeypatch, ci_base_environment
):
    """Verify that in external signing mode: 1) each rebuild jobs includes
    the location where the binary hash information is written and 2) we
    properly generate a final signing job in the pipeline."""
    os.environ.update({"SPACK_PIPELINE_TYPE": "spack_protected_branch"})
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    temporary-storage-url-prefix: file:///work/temp/mirror
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
        build-job:
          tags:
            - donotcare
          image: donotcare
    - signing-job:
        tags:
          - nonbuildtag
          - secretrunner
        image:
          name: customdockerimage
          entrypoint: []
        variables:
          IMPORTANT_INFO: avalue
        script::
          - echo hello
        custom_attribute: custom!
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as of:
            pipeline_doc = syaml.load(of.read())

            assert "sign-pkgs" in pipeline_doc
            signing_job = pipeline_doc["sign-pkgs"]
            assert "tags" in signing_job
            signing_job_tags = signing_job["tags"]
            for expected_tag in ["notary", "protected", "aws"]:
                assert expected_tag in signing_job_tags
            assert signing_job["custom_attribute"] == "custom!"


def test_ci_reproduce(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    last_two_git_commits,
    ci_base_environment,
    mock_binary_index,
):
    working_dir = tmpdir.join("repro_dir")
    image_name = "org/image:tag"

    spack_yaml_contents = """
spack:
 definitions:
   - packages: [archive-files]
 specs:
   - $packages
 mirrors:
   test-mirror: file:///some/fake/mirror
 ci:
   pipeline-gen:
   - submapping:
     - match:
         - archive-files
       build-job:
         tags:
           - donotcare
         image: {0}
""".format(
        image_name
    )

    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(spack_yaml_contents)

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        with ev.read("test") as env:
            with env.write_transaction():
                env.concretize()
                env.write()

            if not os.path.exists(working_dir.strpath):
                os.makedirs(working_dir.strpath)

            shutil.copyfile(env.manifest_path, os.path.join(working_dir.strpath, "spack.yaml"))
            shutil.copyfile(env.lock_path, os.path.join(working_dir.strpath, "spack.lock"))

            job_spec = None

            for h, s in env.specs_by_hash.items():
                if s.name == "archive-files":
                    job_spec = s

            job_spec_json_path = os.path.join(working_dir.strpath, "archivefiles.json")
            with open(job_spec_json_path, "w") as fd:
                fd.write(job_spec.to_json(hash=ht.dag_hash))

            artifacts_root = os.path.join(working_dir.strpath, "scratch_dir")
            pipeline_path = os.path.join(artifacts_root, "pipeline.yml")

            ci_cmd("generate", "--output-file", pipeline_path, "--artifacts-root", artifacts_root)

            job_name = ci.get_job_name(job_spec)

            repro_file = os.path.join(working_dir.strpath, "repro.json")
            repro_details = {
                "job_name": job_name,
                "job_spec_json": "archivefiles.json",
                "ci_project_dir": working_dir.strpath,
            }
            with open(repro_file, "w") as fd:
                fd.write(json.dumps(repro_details))

            install_script = os.path.join(working_dir.strpath, "install.sh")
            with open(install_script, "w") as fd:
                fd.write("#!/bin/bash\n\n#fake install\nspack install blah\n")

            spack_info_file = os.path.join(working_dir.strpath, "spack_info.txt")
            with open(spack_info_file, "w") as fd:
                fd.write(
                    "\nMerge {0} into {1}\n\n".format(
                        last_two_git_commits[1], last_two_git_commits[0]
                    )
                )

    def fake_download_and_extract_artifacts(url, work_dir):
        pass

    monkeypatch.setattr(ci, "download_and_extract_artifacts", fake_download_and_extract_artifacts)
    rep_out = ci_cmd(
        "reproduce-build",
        "https://some.domain/api/v1/projects/1/jobs/2/artifacts",
        "--working-dir",
        working_dir.strpath,
        output=str,
    )
    # Make sure the script was generated
    assert os.path.exists(os.path.join(os.path.realpath(working_dir.strpath), "start.sh"))
    # Make sure we tell the suer where it is when not in interactive mode
    expect_out = "$ {0}/start.sh".format(os.path.realpath(working_dir.strpath))
    assert expect_out in rep_out


@pytest.mark.parametrize(
    "subcmd", [(""), ("generate"), ("rebuild-index"), ("rebuild"), ("reproduce-build")]
)
def test_ci_help(subcmd, capsys):
    """Make sure `spack ci` --help describes the (sub)command help."""
    out = spack.main.SpackCommand("ci", subprocess=True)(subcmd, "--help")

    usage = "usage: spack ci {0}{1}[".format(subcmd, " " if subcmd else "")
    assert usage in out


def test_cmd_first_line():
    """Explicitly test first_line since not picked up in test_ci_help."""
    first = "This is a test."
    doc = """{0}

    Is there more to be said?""".format(
        first
    )

    assert spack.cmd.first_line(doc) == first


legacy_spack_yaml_contents = """
spack:
  definitions:
    - old-gcc-pkgs:
      - archive-files
      - callpath
      # specify ^openblas-with-lapack to ensure that builtin.mock repo flake8
      # package (which can also provide lapack) is not chosen, as it violates
      # a package-level check which requires exactly one fetch strategy (this
      # is apparently not an issue for other tests that use it).
      - hypre@0.2.15 ^openblas-with-lapack
  specs:
    - matrix:
      - [$old-gcc-pkgs]
  mirrors:
    test-mirror: file:///some/fake/mirror
  {0}:
    match_behavior: first
    mappings:
      - match:
          - arch=test-debian6-core2
        runner-attributes:
          tags:
            - donotcare
          image: donotcare
      - match:
          - arch=test-debian6-m1
        runner-attributes:
          tags:
            - donotcare
          image: donotcare
    service-job-attributes:
      image: donotcare
      tags: [donotcare]
  cdash:
    build-group: Not important
    url: https://my.fake.cdash
    project: Not used
    site: Nothing
"""


@pytest.mark.regression("36409")
def test_gitlab_ci_deprecated(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
    mock_binary_index,
):
    mirror_url = "file:///some/fake/mirror"
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(legacy_spack_yaml_contents.format("gitlab-ci"))

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = "generated-pipeline.yaml"

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)

            assert "stages" in yaml_contents
            assert len(yaml_contents["stages"]) == 5
            assert yaml_contents["stages"][0] == "stage-0"
            assert yaml_contents["stages"][4] == "stage-rebuild-index"

            assert "rebuild-index" in yaml_contents
            rebuild_job = yaml_contents["rebuild-index"]
            expected = "spack buildcache update-index --keys {0}".format(mirror_url)
            assert rebuild_job["script"][0] == expected

            assert "variables" in yaml_contents
            assert "SPACK_ARTIFACTS_ROOT" in yaml_contents["variables"]
            artifacts_root = yaml_contents["variables"]["SPACK_ARTIFACTS_ROOT"]
            assert artifacts_root == "jobs_scratch_dir"


@pytest.mark.regression("36045")
def test_gitlab_ci_update(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
    mock_binary_index,
):
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(legacy_spack_yaml_contents.format("ci"))

    with tmpdir.as_cwd():
        env_cmd("update", "-y", ".")

        with open("spack.yaml") as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)

            ci_root = yaml_contents["spack"]["ci"]

            assert "pipeline-gen" in ci_root


def test_gitlab_config_scopes(
    tmpdir, working_env, mutable_mock_env_path, mock_packages, ci_base_environment
):
    """Test pipeline generation with real configs included"""
    configs_path = os.path.join(spack_paths.share_path, "gitlab", "cloud_pipelines", "configs")
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  config:
    install_tree: {0}
  include: [{1}]
  view: false
  specs:
    - flatten-deps
  mirrors:
    some-mirror: https://my.fake.mirror
  ci:
    pipeline-gen:
    - build-job:
        image: "ecpe4s/ubuntu20.04-runner-x86_64:2023-01-01"
        tags: ["some_tag"]
""".format(
                tmpdir.strpath, configs_path
            )
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)

        with open(outputfile) as f:
            contents = f.read()
            yaml_contents = syaml.load(contents)

            assert "rebuild-index" in yaml_contents
            rebuild_job = yaml_contents["rebuild-index"]
            assert "tags" in rebuild_job
            assert "variables" in rebuild_job
            rebuild_tags = rebuild_job["tags"]
            rebuild_vars = rebuild_job["variables"]
            assert all([t in rebuild_tags for t in ["spack", "service"]])
            expected_vars = ["CI_JOB_SIZE", "KUBERNETES_CPU_REQUEST", "KUBERNETES_MEMORY_REQUEST"]
            assert all([v in rebuild_vars for v in expected_vars])


def test_ci_generate_mirror_config(
    tmpdir,
    mutable_mock_env_path,
    install_mockery,
    mock_packages,
    monkeypatch,
    ci_base_environment,
    mock_binary_index,
):
    """Make sure the correct mirror gets used as the buildcache destination"""
    filename = str(tmpdir.join("spack.yaml"))
    with open(filename, "w") as f:
        f.write(
            """\
spack:
  specs:
    - archive-files
  mirrors:
    some-mirror: file:///this/is/a/source/mirror
    buildcache-destination: file:///push/binaries/here
  ci:
    pipeline-gen:
    - submapping:
      - match:
          - archive-files
        build-job:
          tags:
            - donotcare
          image: donotcare
"""
        )

    with tmpdir.as_cwd():
        env_cmd("create", "test", "./spack.yaml")
        outputfile = str(tmpdir.join(".gitlab-ci.yml"))

        with ev.read("test"):
            ci_cmd("generate", "--output-file", outputfile)
            with open(outputfile) as of:
                pipeline_doc = syaml.load(of.read())
                assert "rebuild-index" in pipeline_doc
                reindex_job = pipeline_doc["rebuild-index"]
                assert "script" in reindex_job
                reindex_step = reindex_job["script"][0]
                assert "file:///push/binaries/here" in reindex_step
