From ae93d8be8a5f2d38e67de25ad97d3130abaaa8f5 Mon Sep 17 00:00:00 2001 From: "A. Alonso Dominguez" <2269440+alonsodomin@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:47:14 +0200 Subject: [PATCH] Improve handling of additional files in Helm unit tests (#19263) Fixes handling of additional files in Helm unit tests --- docs/markdown/Helm/helm-overview.md | 95 +++++++ .../helm/dependency_inference/deployment.py | 2 +- .../pants/backend/helm/test/unittest.py | 22 +- .../pants/backend/helm/test/unittest_test.py | 267 +++++++++++++++++- 4 files changed, 369 insertions(+), 17 deletions(-) diff --git a/docs/markdown/Helm/helm-overview.md b/docs/markdown/Helm/helm-overview.md index 65802ec444b..a70e778da39 100644 --- a/docs/markdown/Helm/helm-overview.md +++ b/docs/markdown/Helm/helm-overview.md @@ -158,6 +158,101 @@ pants test :: ✓ testprojects/src/helm/example/tests/env-configmap_test.yaml succeeded in 0.75s. ``` +### Feeding additional files to unit tests + +In some cases we may want our tests to have access to additional files which are not part of the chart. This can be achieved by setting a dependency between our unit test targets and a `resources` target as follows: + +```python src/helm/example/tests/BUILD +helm_unittest_tests(dependencies=[":extra-values"]) + +resources(name="extra-values", sources=["extra-values.yml"]) +``` +```yaml src/helm/example/templates/env-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: example-configmap +data: +{{- range $key, $val := .Values.data }} + {{ $key | upper }}: {{ $val | quote }} +{{- end }} +``` +```yaml src/helm/example/tests/extra-values.yml +data: + VAR1_NAME: var1Value + var2_name: var2Value +``` +```yaml src/helm/example/tests/env-configmap_test.yaml +suite: test env-configmap +templates: + - env-configmap.yaml +values: + - extra-values.yml +tests: + - it: should contain the env map variables + asserts: + - equal: + path: data.VAR1_NAME + value: "var1Value" + - equal: + path: data.VAR2_NAME + value: "var2Value" +``` + +Additional files can be referenced from any location inside your workspace. Note that the actual path to the additional files will be relative to the source roots configured in Pants. + +In this example, since Helm charts define their source root at the location of the `Chart.yaml` file and the `extra-values.yml` file is inside the `tests` folder relative to the chart, the test suite can access it as being local to it. + +However, in the following case, we need to reference the extra file relative to the chart root. Note the `../data/extra-values.yml` path in the test suite. + +```toml pants.toml +[source] +root_patterns=["src/extra"] +``` +```python src/extra/data/BUILD +resources(name="extra-values", sources=["extra-values.yml"]) +``` +```yaml src/extra/data/extra-values.yml +data: + VAR1_NAME: var1Value + var2_name: var2Value +``` +```python src/helm/example/tests/BUILD +helm_unittest_tests(dependencies=["src/extra/data:extra-values"]) +``` +```yaml src/helm/example/templates/env-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: example-configmap +data: +{{- range $key, $val := .Values.data }} + {{ $key | upper }}: {{ $val | quote }} +{{- end }} +``` +```yaml src/helm/example/tests/env-configmap_test.yaml +suite: test env-configmap +templates: + - env-configmap.yaml +values: + - ../data/extra-values.yml +tests: + - it: should contain the env map variables + asserts: + - equal: + path: data.VAR1_NAME + value: "var1Value" + - equal: + path: data.VAR2_NAME + value: "var2Value" +``` + +> 🚧 Using `file`, `files` and `relocated_files` targets +> +> Other file-centric targets are also supported, just be aware that `file` and `files` targets are +> not affected by the source roots setting. When using `relocated_files`, the files will be relative +> to the value set in the `dest` field. + ### Timeouts Pants can cancel tests that take too long, which is useful to prevent tests from hanging indefinitely. diff --git a/src/python/pants/backend/helm/dependency_inference/deployment.py b/src/python/pants/backend/helm/dependency_inference/deployment.py index 0612077ee9a..6354b4093c0 100644 --- a/src/python/pants/backend/helm/dependency_inference/deployment.py +++ b/src/python/pants/backend/helm/dependency_inference/deployment.py @@ -132,7 +132,7 @@ async def first_party_helm_deployment_mapping( docker_target_addresses = {tgt.address.spec: tgt.address for tgt in docker_targets} def lookup_docker_addreses(image_ref: str) -> tuple[str, Address] | None: - addr = docker_target_addresses.get(str(image_ref), None) + addr = docker_target_addresses.get(image_ref, None) if addr: return image_ref, addr return None diff --git a/src/python/pants/backend/helm/test/unittest.py b/src/python/pants/backend/helm/test/unittest.py index ad6d6936336..7206f315b25 100644 --- a/src/python/pants/backend/helm/test/unittest.py +++ b/src/python/pants/backend/helm/test/unittest.py @@ -26,8 +26,9 @@ from pants.backend.helm.util_rules.tool import HelmProcess from pants.base.deprecated import warn_or_error from pants.core.goals.test import TestFieldSet, TestRequest, TestResult, TestSubsystem -from pants.core.target_types import ResourceSourceField +from pants.core.target_types import FileSourceField, ResourceSourceField from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest +from pants.core.util_rules.stripped_source_files import StrippedSourceFiles from pants.engine.addresses import Address from pants.engine.fs import AddPrefix, Digest, MergeDigests, RemovePrefix, Snapshot from pants.engine.process import FallibleProcessResult, ProcessCacheScope @@ -89,21 +90,18 @@ async def run_helm_unittest( raise MissingUnitTestChartDependency(field_set.address) chart_target = chart_targets[0] - chart, chart_root, test_files = await MultiGet( + chart, chart_root, test_files, extra_files = await MultiGet( Get(HelmChart, HelmChartRequest, HelmChartRequest.from_target(chart_target)), Get(HelmChartRoot, HelmChartRootRequest(chart_target[HelmChartMetaSourceField])), Get( SourceFiles, + SourceFilesRequest(sources_fields=[field_set.source]), + ), + Get( + StrippedSourceFiles, SourceFilesRequest( - sources_fields=[ - field_set.source, - *( - tgt.get(SourcesField) - for tgt in transitive_targets.dependencies - if not HelmChartFieldSet.is_applicable(tgt) - ), - ], - for_sources_types=(HelmUnitTestSourceField, ResourceSourceField), + sources_fields=[tgt.get(SourcesField) for tgt in transitive_targets.dependencies], + for_sources_types=(ResourceSourceField, FileSourceField), enable_codegen=True, ), ), @@ -118,7 +116,7 @@ async def run_helm_unittest( merged_digests = await Get( Digest, - MergeDigests([chart.snapshot.digest, stripped_test_files]), + MergeDigests([chart.snapshot.digest, stripped_test_files, extra_files.snapshot.digest]), ) input_digest = await Get(Digest, AddPrefix(merged_digests, chart.name)) diff --git a/src/python/pants/backend/helm/test/unittest_test.py b/src/python/pants/backend/helm/test/unittest_test.py index ffaa3fda6d8..6e6e9e852e4 100644 --- a/src/python/pants/backend/helm/test/unittest_test.py +++ b/src/python/pants/backend/helm/test/unittest_test.py @@ -5,8 +5,12 @@ import pytest -from pants.backend.helm.target_types import HelmChartTarget, HelmUnitTestTestTarget -from pants.backend.helm.target_types import rules as target_types_rules +from pants.backend.helm.target_types import ( + HelmChartTarget, + HelmUnitTestTestsGeneratorTarget, + HelmUnitTestTestTarget, +) +from pants.backend.helm.target_types import rules as helm_target_types_rules from pants.backend.helm.test.unittest import HelmUnitTestFieldSet, HelmUnitTestRequest from pants.backend.helm.test.unittest import rules as unittest_rules from pants.backend.helm.testutil import ( @@ -17,7 +21,14 @@ ) from pants.backend.helm.util_rules import chart from pants.core.goals.test import TestResult -from pants.core.util_rules import external_tool, source_files +from pants.core.target_types import ( + FilesGeneratorTarget, + FileTarget, + RelocatedFiles, + ResourcesGeneratorTarget, +) +from pants.core.target_types import rules as target_types_rules +from pants.core.util_rules import external_tool, source_files, stripped_source_files from pants.engine.addresses import Address from pants.engine.rules import QueryRule from pants.source.source_root import rules as source_root_rules @@ -27,13 +38,23 @@ @pytest.fixture def rule_runner() -> RuleRunner: return RuleRunner( - target_types=[HelmChartTarget, HelmUnitTestTestTarget], + target_types=[ + HelmChartTarget, + HelmUnitTestTestTarget, + HelmUnitTestTestsGeneratorTarget, + ResourcesGeneratorTarget, + FileTarget, + FilesGeneratorTarget, + RelocatedFiles, + ], rules=[ *external_tool.rules(), *chart.rules(), *unittest_rules(), *source_files.rules(), *source_root_rules(), + *helm_target_types_rules(), + *stripped_source_files.rules(), *target_types_rules(), QueryRule(TestResult, (HelmUnitTestRequest.Batch,)), ], @@ -153,3 +174,241 @@ def test_simple_failure(rule_runner: RuleRunner) -> None: result = rule_runner.request(TestResult, [HelmUnitTestRequest.Batch("", (field_set,), None)]) assert result.exit_code == 1 + + +def test_test_with_local_resource_file(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "BUILD": "helm_chart(name='mychart')", + "Chart.yaml": HELM_CHART_FILE, + "templates/configmap.yaml": dedent( + """\ + apiVersion: v1 + kind: ConfigMap + metadata: + name: foo-config + data: + foo_key: {{ .Values.input }} + """ + ), + "tests/BUILD": dedent( + """\ + helm_unittest_test(name="test", source="configmap_test.yaml", dependencies=[":values"]) + + resources(name="values", sources=["general-values.yml"]) + """ + ), + "tests/configmap_test.yaml": dedent( + """\ + suite: test config map + templates: + - configmap.yaml + values: + - general-values.yml + tests: + - it: should work + asserts: + - equal: + path: data.foo_key + value: bar_input + """ + ), + "tests/general-values.yml": dedent( + """\ + input: bar_input + """ + ), + } + ) + + target = rule_runner.get_target(Address("tests", target_name="test")) + field_set = HelmUnitTestFieldSet.create(target) + + result = rule_runner.request(TestResult, [HelmUnitTestRequest.Batch("", (field_set,), None)]) + assert result.exit_code == 0 + + +def test_test_with_non_local_resource_file(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "src/helm/BUILD": "helm_chart(name='mychart')", + "src/helm/Chart.yaml": HELM_CHART_FILE, + "src/helm/templates/configmap.yaml": dedent( + """\ + apiVersion: v1 + kind: ConfigMap + metadata: + name: foo-config + data: + foo_key: {{ .Values.input }} + """ + ), + "src/helm/tests/BUILD": dedent( + """\ + helm_unittest_test( + name="test", + source="configmap_test.yaml", + dependencies=["//resources/data:values"] + ) + """ + ), + "src/helm/tests/configmap_test.yaml": dedent( + """\ + suite: test config map + templates: + - configmap.yaml + values: + - ../data/general-values.yml + tests: + - it: should work + asserts: + - equal: + path: data.foo_key + value: bar_input + """ + ), + "resources/data/BUILD": dedent( + """\ + resources(name="values", sources=["general-values.yml"]) + """ + ), + "resources/data/general-values.yml": dedent( + """\ + input: bar_input + """ + ), + } + ) + + source_roots = ["resources"] + rule_runner.set_options([f"--source-root-patterns={repr(source_roots)}"]) + target = rule_runner.get_target(Address("src/helm/tests", target_name="test")) + field_set = HelmUnitTestFieldSet.create(target) + + result = rule_runner.request(TestResult, [HelmUnitTestRequest.Batch("", (field_set,), None)]) + assert result.exit_code == 0 + + +def test_test_with_global_file(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "src/helm/BUILD": "helm_chart(name='mychart')", + "src/helm/Chart.yaml": HELM_CHART_FILE, + "src/helm/templates/configmap.yaml": dedent( + """\ + apiVersion: v1 + kind: ConfigMap + metadata: + name: foo-config + data: + foo_key: {{ .Values.input }} + """ + ), + "src/helm/tests/BUILD": dedent( + """\ + helm_unittest_test( + name="test", + source="configmap_test.yaml", + dependencies=["//files/data:values"] + ) + """ + ), + "src/helm/tests/configmap_test.yaml": dedent( + """\ + suite: test config map + templates: + - configmap.yaml + values: + - ../files/data/general-values.yml + tests: + - it: should work + asserts: + - equal: + path: data.foo_key + value: bar_input + """ + ), + "files/data/BUILD": dedent( + """\ + files(name="values", sources=["general-values.yml"]) + """ + ), + "files/data/general-values.yml": dedent( + """\ + input: bar_input + """ + ), + } + ) + + target = rule_runner.get_target(Address("src/helm/tests", target_name="test")) + field_set = HelmUnitTestFieldSet.create(target) + + result = rule_runner.request(TestResult, [HelmUnitTestRequest.Batch("", (field_set,), None)]) + assert result.exit_code == 0 + + +def test_test_with_relocated_file(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "src/helm/BUILD": "helm_chart(name='mychart')", + "src/helm/Chart.yaml": HELM_CHART_FILE, + "src/helm/templates/configmap.yaml": dedent( + """\ + apiVersion: v1 + kind: ConfigMap + metadata: + name: foo-config + data: + foo_key: {{ .Values.input }} + """ + ), + "src/helm/tests/BUILD": dedent( + """\ + helm_unittest_test( + name="test", + source="configmap_test.yaml", + dependencies=[":relocated"] + ) + + relocated_files( + name="relocated", + files_targets=["//files/data:values"], + src="files/data", + dest="tests" + ) + """ + ), + "src/helm/tests/configmap_test.yaml": dedent( + """\ + suite: test config map + templates: + - configmap.yaml + values: + - general-values.yml + tests: + - it: should work + asserts: + - equal: + path: data.foo_key + value: bar_input + """ + ), + "files/data/BUILD": dedent( + """\ + files(name="values", sources=["general-values.yml"]) + """ + ), + "files/data/general-values.yml": dedent( + """\ + input: bar_input + """ + ), + } + ) + + target = rule_runner.get_target(Address("src/helm/tests", target_name="test")) + field_set = HelmUnitTestFieldSet.create(target) + + result = rule_runner.request(TestResult, [HelmUnitTestRequest.Batch("", (field_set,), None)]) + assert result.exit_code == 0