diff --git a/docs/markdown/Java and Scala/jvm-overview.md b/docs/markdown/Java and Scala/jvm-overview.md index cf2a3a078a6a..89ebbb070819 100644 --- a/docs/markdown/Java and Scala/jvm-overview.md +++ b/docs/markdown/Java and Scala/jvm-overview.md @@ -215,6 +215,32 @@ If a target sets its `timeout` higher than `[test].timeout_maximum`, Pants will Use the option `./pants test --no-timeouts` to temporarily disable timeouts, e.g. when debugging. +### Setting environment variables + +Test runs are _hermetic_, meaning that they are stripped of the parent `./pants` process's environment variables. This is important for reproducibility, and it also increases cache hits. + +To add any arbitrary environment variable back to the process, you can either add the environment variable to the specific tests with the `extra_env_vars` field on `junit_test` / `junit_tests` / `scala_junit_test` / `scala_junit_tests` / `scalatest_test` / `scalatest_tests` targets or to all your tests with the `[test].extra_env_vars` option. Generally, prefer the field `extra_env_vars` field so that more of your tests are hermetic. + +With both `[test].extra_env_vars` and the `extra_env_vars` field, you can either hardcode a value or leave off a value to "allowlist" it and read from the parent `./pants` process's environment. + +```toml pants.toml +[test] +extra_env_vars = ["VAR1", "VAR2=hardcoded_value"] +``` +```python project/BUILD +junit_tests( + name="tests", + # Adds to all generated `junit_test` targets, + # i.e. each file in the `sources` field. + extra_env_vars=["VAR3", "VAR4=hardcoded"], + # Even better, use `overrides` to be more granular. + overrides={ + "StrUtilTest.java": {"extra_env_vars": ["VAR"]}, + ("DirUtilTest.java", "OSUtilTest.java"): {"extra_env_vars": ["VAR5"]}, + }, +) +``` + Lint and Format --------------- diff --git a/docs/markdown/Java and Scala/kotlin.md b/docs/markdown/Java and Scala/kotlin.md index 2a1239e49747..bc68268fbee0 100644 --- a/docs/markdown/Java and Scala/kotlin.md +++ b/docs/markdown/Java and Scala/kotlin.md @@ -226,6 +226,32 @@ To run tests, use `./pants test`: The Kotlin backend currently supports JUnit tests specified using the `kotlin_junit_tests` target type. +### Setting environment variables + +Test runs are _hermetic_, meaning that they are stripped of the parent `./pants` process's environment variables. This is important for reproducibility, and it also increases cache hits. + +To add any arbitrary environment variable back to the process, you can either add the environment variable to the specific tests with the `extra_env_vars` field on `kotlin_junit_test` / `kotlin_junit_tests` targets or to all your tests with the `[test].extra_env_vars` option. Generally, prefer the field `extra_env_vars` field so that more of your tests are hermetic. + +With both `[test].extra_env_vars` and the `extra_env_vars` field, you can either hardcode a value or leave off a value to "allowlist" it and read from the parent `./pants` process's environment. + +```toml pants.toml +[test] +extra_env_vars = ["VAR1", "VAR2=hardcoded_value"] +``` +```python project/BUILD +kotlin_junit_tests( + name="tests", + # Adds to all generated `kotlin_junit_test` targets, + # i.e. each file in the `sources` field. + extra_env_vars=["VAR3", "VAR4=hardcoded"], + # Even better, use `overrides` to be more granular. + overrides={ + "StrUtilTest.kt": {"extra_env_vars": ["VAR"]}, + ("DirUtilTest.kt", "OSUtilTest.kt"): {"extra_env_vars": ["VAR5"]}, + }, +) +``` + ## Lint and Format [`ktlint`](https://ktlint.github.io/) can be enabled by adding the `pants.backend.experimental.kotlin.lint.ktlint` diff --git a/src/python/pants/backend/go/target_types.py b/src/python/pants/backend/go/target_types.py index fb295f8efc6d..39244f9ff02b 100644 --- a/src/python/pants/backend/go/target_types.py +++ b/src/python/pants/backend/go/target_types.py @@ -9,7 +9,7 @@ from pants.core.goals.package import OutputPathField from pants.core.goals.run import RestartableField -from pants.core.goals.test import TestTimeoutField +from pants.core.goals.test import TestExtraEnvVarsField, TestTimeoutField from pants.engine.addresses import Address from pants.engine.engine_aware import EngineAwareParameter from pants.engine.target import ( @@ -21,7 +21,6 @@ InvalidTargetException, MultipleSourcesField, StringField, - StringSequenceField, Target, TargetGenerator, ValidNumbers, @@ -180,16 +179,8 @@ class SkipGoTestsField(BoolField): help = "If true, don't run this package's tests." -class GoTestExtraEnvVarsField(StringSequenceField): +class GoTestExtraEnvVarsField(TestExtraEnvVarsField): alias = "test_extra_env_vars" - help = softwrap( - """ - Additional environment variables to include in test processes. - Entries are strings in the form `ENV_VAR=value` to use explicitly; or just - `ENV_VAR` to copy the value of a variable in Pants's own environment. - This will be merged with and override values from [test].extra_env_vars. - """ - ) class GoTestTimeoutField(TestTimeoutField): diff --git a/src/python/pants/backend/java/target_types.py b/src/python/pants/backend/java/target_types.py index ddb2177dacda..b62a47d490ae 100644 --- a/src/python/pants/backend/java/target_types.py +++ b/src/python/pants/backend/java/target_types.py @@ -17,6 +17,7 @@ ) from pants.jvm import target_types as jvm_target_types from pants.jvm.target_types import ( + JunitTestExtraEnvVarsField, JunitTestSourceField, JunitTestTimeoutField, JvmDependenciesField, @@ -63,6 +64,7 @@ class JunitTestTarget(Target): *COMMON_TARGET_FIELDS, JavaJunitTestSourceField, JunitTestTimeoutField, + JunitTestExtraEnvVarsField, JvmDependenciesField, JvmResolveField, JvmProvidesTypesField, @@ -80,11 +82,15 @@ class JavaTestsGeneratorSourcesField(JavaGeneratorSourcesField): class JunitTestsGeneratorTarget(TargetFilesGenerator): alias = "junit_tests" - core_fields = (*COMMON_TARGET_FIELDS, JavaTestsGeneratorSourcesField, JunitTestTimeoutField) + core_fields = ( + *COMMON_TARGET_FIELDS, + JavaTestsGeneratorSourcesField, + ) generated_target_cls = JunitTestTarget copied_fields = COMMON_TARGET_FIELDS moved_fields = ( JunitTestTimeoutField, + JunitTestExtraEnvVarsField, JvmDependenciesField, JvmJdkField, JvmProvidesTypesField, diff --git a/src/python/pants/backend/kotlin/target_types.py b/src/python/pants/backend/kotlin/target_types.py index 62d37e3d96dd..d6f226667fd5 100644 --- a/src/python/pants/backend/kotlin/target_types.py +++ b/src/python/pants/backend/kotlin/target_types.py @@ -20,6 +20,7 @@ generate_multiple_sources_field_help_message, ) from pants.jvm.target_types import ( + JunitTestExtraEnvVarsField, JunitTestSourceField, JunitTestTimeoutField, JvmJdkField, @@ -137,6 +138,7 @@ class KotlinJunitTestTarget(Target): KotlinJunitTestSourceField, KotlincConsumedPluginIdsField, JunitTestTimeoutField, + JunitTestExtraEnvVarsField, JvmResolveField, JvmJdkField, JvmProvidesTypesField, @@ -156,7 +158,6 @@ class KotlinJunitTestsGeneratorTarget(TargetFilesGenerator): core_fields = ( *COMMON_TARGET_FIELDS, KotlinJunitTestsGeneratorSourcesField, - JunitTestTimeoutField, ) generated_target_cls = KotlinJunitTestTarget copied_fields = COMMON_TARGET_FIELDS @@ -164,6 +165,7 @@ class KotlinJunitTestsGeneratorTarget(TargetFilesGenerator): KotlinJunitTestDependenciesField, KotlincConsumedPluginIdsField, JunitTestTimeoutField, + JunitTestExtraEnvVarsField, JvmResolveField, JvmJdkField, JvmProvidesTypesField, diff --git a/src/python/pants/backend/kotlin/test/junit_test.py b/src/python/pants/backend/kotlin/test/junit_test.py index 6132488b6ee1..48a0056b77b4 100644 --- a/src/python/pants/backend/kotlin/test/junit_test.py +++ b/src/python/pants/backend/kotlin/test/junit_test.py @@ -19,7 +19,7 @@ from pants.backend.kotlin.subsystems.kotlin import DEFAULT_KOTLIN_VERSION from pants.backend.kotlin.target_types import KotlinJunitTestsGeneratorTarget from pants.backend.kotlin.test.junit import rules as kotlin_junit_rules -from pants.core.goals.test import TestResult +from pants.core.goals.test import TestResult, get_filtered_environment from pants.core.target_types import FilesGeneratorTarget, FileTarget, RelocatedFiles from pants.core.util_rules import config_files, source_files from pants.core.util_rules.external_tool import rules as external_tool_rules @@ -35,6 +35,7 @@ from pants.jvm.test.junit import JunitTestFieldSet from pants.jvm.test.junit import rules as jvm_junit_rules from pants.jvm.test.junit_test import run_junit_test +from pants.jvm.testutil import maybe_skip_jdk_test from pants.jvm.util_rules import rules as util_rules from pants.testutil.rule_runner import PYTHON_BOOTSTRAP_ENV, QueryRule, RuleRunner @@ -78,6 +79,7 @@ def rule_runner() -> RuleRunner: *kotlinc.rules(), *kotlinc_plugins.rules(), *kotlin_dep_inf_rules(), + get_filtered_environment, QueryRule(CoarsenedTargets, (Addresses,)), QueryRule(TestResult, (JunitTestFieldSet,)), ], @@ -97,6 +99,7 @@ def rule_runner() -> RuleRunner: return rule_runner +@maybe_skip_jdk_test def test_vintage_kotlin_simple_success( rule_runner: RuleRunner, jvm_lockfile: JVMLockfileFixture ) -> None: @@ -135,3 +138,59 @@ def test_vintage_kotlin_simple_success( assert re.search(r"Finished:\s+testHello", test_result.stdout) is not None assert re.search(r"1 tests successful", test_result.stdout) is not None assert re.search(r"1 tests found", test_result.stdout) is not None + + +@maybe_skip_jdk_test +def test_vintage_extra_env_vars(rule_runner: RuleRunner, jvm_lockfile: JVMLockfileFixture) -> None: + rule_runner.write_files( + { + "3rdparty/jvm/default.lock": jvm_lockfile.serialized_lockfile, + "3rdparty/jvm/BUILD": jvm_lockfile.requirements_as_jvm_artifact_targets(), + "BUILD": dedent( + """\ + kotlin_junit_tests( + name='example-test', + extra_env_vars=[ + "JUNIT_TESTS_VAR_WITHOUT_VALUE", + "JUNIT_TESTS_VAR_WITH_VALUE=junit_tests_var_with_value", + "JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR=junit_tests_override_with_value_var_override", + ], + ) + """ + ), + "ExtraEnvVarsTest.kt": dedent( + """ + package org.pantsbuild.example + + import kotlin.test.Test + import kotlin.test.assertEquals + + internal class ExtraEnvVarsTest { + @Test + fun testArgs() { + assertEquals(System.getenv("ARG_WITH_VALUE_VAR"), "arg_with_value_var"); + assertEquals(System.getenv("ARG_WITHOUT_VALUE_VAR"), "arg_without_value_var"); + assertEquals(System.getenv("JUNIT_TESTS_VAR_WITH_VALUE"), "junit_tests_var_with_value"); + assertEquals(System.getenv("JUNIT_TESTS_VAR_WITHOUT_VALUE"), "junit_tests_var_without_value"); + assertEquals(System.getenv("JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR"), "junit_tests_override_with_value_var_override"); + } + } + """ + ), + } + ) + + test_result = run_junit_test( + rule_runner, + "example-test", + "ExtraEnvVarsTest.kt", + extra_args=[ + '--test-extra-env-vars=["ARG_WITH_VALUE_VAR=arg_with_value_var", "ARG_WITHOUT_VALUE_VAR", "JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR"]' + ], + env={ + "ARG_WITHOUT_VALUE_VAR": "arg_without_value_var", + "JUNIT_TESTS_VAR_WITHOUT_VALUE": "junit_tests_var_without_value", + "JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR": "junit_tests_override_with_value_var", + }, + ) + assert test_result.exit_code == 0 diff --git a/src/python/pants/backend/python/target_types.py b/src/python/pants/backend/python/target_types.py index e770e87f50c2..482140642557 100644 --- a/src/python/pants/backend/python/target_types.py +++ b/src/python/pants/backend/python/target_types.py @@ -32,7 +32,11 @@ from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError from pants.core.goals.package import OutputPathField from pants.core.goals.run import RestartableField -from pants.core.goals.test import RuntimePackageDependenciesField, TestSubsystem +from pants.core.goals.test import ( + RuntimePackageDependenciesField, + TestExtraEnvVarsField, + TestSubsystem, +) from pants.engine.addresses import Address, Addresses from pants.engine.target import ( COMMON_TARGET_FIELDS, @@ -850,16 +854,8 @@ def resolve_opt(*, option: str) -> Any: return result -class PythonTestsExtraEnvVarsField(StringSequenceField): - alias = "extra_env_vars" - help = softwrap( - """ - Additional environment variables to include in test processes. - Entries are strings in the form `ENV_VAR=value` to use explicitly; or just - `ENV_VAR` to copy the value of a variable in Pants's own environment. - This will be merged with and override values from [test].extra_env_vars. - """ - ) +class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField): + pass class SkipPythonTestsField(BoolField): diff --git a/src/python/pants/backend/scala/target_types.py b/src/python/pants/backend/scala/target_types.py index 7e6080c924de..a49e59eb8a69 100644 --- a/src/python/pants/backend/scala/target_types.py +++ b/src/python/pants/backend/scala/target_types.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from pants.backend.scala.subsystems.scala_infer import ScalaInferSubsystem -from pants.core.goals.test import TestTimeoutField +from pants.core.goals.test import TestExtraEnvVarsField, TestTimeoutField from pants.engine.rules import collect_rules, rule from pants.engine.target import ( COMMON_TARGET_FIELDS, @@ -28,6 +28,7 @@ from pants.engine.unions import UnionRule from pants.jvm import target_types as jvm_target_types from pants.jvm.target_types import ( + JunitTestExtraEnvVarsField, JunitTestSourceField, JunitTestTimeoutField, JvmJdkField, @@ -107,6 +108,10 @@ class ScalatestTestTimeoutField(TestTimeoutField): pass +class ScalatestTestExtraEnvVarsField(TestExtraEnvVarsField): + pass + + class ScalatestTestTarget(Target): alias = "scalatest_test" core_fields = ( @@ -115,6 +120,7 @@ class ScalatestTestTarget(Target): ScalatestTestSourceField, ScalaConsumedPluginNamesField, ScalatestTestTimeoutField, + ScalatestTestExtraEnvVarsField, JvmResolveField, JvmProvidesTypesField, JvmJdkField, @@ -148,7 +154,6 @@ class ScalatestTestsGeneratorTarget(TargetFilesGenerator): *COMMON_TARGET_FIELDS, ScalatestTestsGeneratorSourcesField, ScalatestTestsSourcesOverridesField, - ScalatestTestTimeoutField, ) generated_target_cls = ScalatestTestTarget copied_fields = COMMON_TARGET_FIELDS @@ -156,6 +161,7 @@ class ScalatestTestsGeneratorTarget(TargetFilesGenerator): ScalaDependenciesField, ScalaConsumedPluginNamesField, ScalatestTestTimeoutField, + ScalatestTestExtraEnvVarsField, JvmJdkField, JvmProvidesTypesField, JvmResolveField, @@ -186,6 +192,7 @@ class ScalaJunitTestTarget(Target): ScalaJunitTestSourceField, ScalaConsumedPluginNamesField, JunitTestTimeoutField, + JunitTestExtraEnvVarsField, JvmResolveField, JvmProvidesTypesField, JvmJdkField, @@ -227,6 +234,7 @@ class ScalaJunitTestsGeneratorTarget(TargetFilesGenerator): ScalaDependenciesField, ScalaConsumedPluginNamesField, JunitTestTimeoutField, + JunitTestExtraEnvVarsField, JvmJdkField, JvmProvidesTypesField, JvmResolveField, diff --git a/src/python/pants/backend/scala/test/BUILD b/src/python/pants/backend/scala/test/BUILD index 98a4e0f3dfad..686d8cb9ceff 100644 --- a/src/python/pants/backend/scala/test/BUILD +++ b/src/python/pants/backend/scala/test/BUILD @@ -3,6 +3,5 @@ python_sources() -python_tests( - name="tests", -) +python_tests(name="tests", dependencies=[":test_resources"]) +resources(name="test_resources", sources=["*.test.lock"]) diff --git a/src/python/pants/backend/scala/test/scalatest.py b/src/python/pants/backend/scala/test/scalatest.py index de6f05867beb..abc93227a392 100644 --- a/src/python/pants/backend/scala/test/scalatest.py +++ b/src/python/pants/backend/scala/test/scalatest.py @@ -6,11 +6,16 @@ from dataclasses import dataclass from pants.backend.scala.subsystems.scalatest import Scalatest -from pants.backend.scala.target_types import ScalatestTestSourceField, ScalatestTestTimeoutField +from pants.backend.scala.target_types import ( + ScalatestTestExtraEnvVarsField, + ScalatestTestSourceField, + ScalatestTestTimeoutField, +) from pants.core.goals.generate_lockfiles import GenerateToolLockfileSentinel from pants.core.goals.test import ( TestDebugAdapterRequest, TestDebugRequest, + TestExtraEnv, TestFieldSet, TestResult, TestSubsystem, @@ -18,6 +23,7 @@ from pants.core.target_types import FileSourceField from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest from pants.engine.addresses import Addresses +from pants.engine.environment import Environment, EnvironmentRequest from pants.engine.fs import Digest, DigestSubset, MergeDigests, PathGlobs, RemovePrefix, Snapshot from pants.engine.process import ( FallibleProcessResult, @@ -52,6 +58,7 @@ class ScalatestTestFieldSet(TestFieldSet): timeout: ScalatestTestTimeoutField jdk_version: JvmJdkField dependencies: JvmDependenciesField + extra_env_vars: ScalatestTestExtraEnvVarsField class ScalatestToolLockfileSentinel(GenerateJvmToolLockfileSentinel): @@ -76,6 +83,7 @@ async def setup_scalatest_for_target( jvm: JvmSubsystem, scalatest: Scalatest, test_subsystem: TestSubsystem, + test_extra_env: TestExtraEnv, ) -> TestSetup: jdk, transitive_tgts = await MultiGet( @@ -119,6 +127,10 @@ async def setup_scalatest_for_target( if request.is_debug: extra_jvm_args.extend(jvm.debug_args) + field_set_extra_env = await Get( + Environment, EnvironmentRequest(request.field_set.extra_env_vars.value or ()) + ) + process = JvmProcess( jdk=jdk, classpath_entries=[ @@ -138,6 +150,7 @@ async def setup_scalatest_for_target( *scalatest.args, ], input_digest=input_digest, + extra_env={**test_extra_env.env, **field_set_extra_env}, extra_jvm_options=scalatest.jvm_options, extra_immutable_input_digests=extra_immutable_input_digests, output_directories=(reports_dir,), diff --git a/src/python/pants/backend/scala/test/scalatest.test.lock b/src/python/pants/backend/scala/test/scalatest.test.lock new file mode 100644 index 000000000000..a802c7a2fde8 --- /dev/null +++ b/src/python/pants/backend/scala/test/scalatest.test.lock @@ -0,0 +1,1192 @@ +# This lockfile was autogenerated by Pants. To regenerate, run: +# +# ./pants generate-lockfiles +# +# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- +# { +# "version": 1, +# "generated_with_requirements": [ +# "org.scalatest:scalatest_2.13:3.2.10,url=not_provided,jar=not_provided" +# ] +# } +# --- END PANTS LOCKFILE METADATA --- + +[[entries]] +file_name = "org.scala-lang.modules_scala-xml_2.13_1.3.0.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + + +[entries.coord] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" +[entries.file_digest] +fingerprint = "6d96d45a7fc6fc7ab69bdbac841b48cf67ab109f048c8db375ae4effae524f39" +serialized_bytes_length = 571493 +[[entries]] +directDependencies = [] +dependencies = [] +file_name = "org.scala-lang_scala-library_2.13.6.jar" + +[entries.coord] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" +[entries.file_digest] +fingerprint = "f19ed732e150d3537794fd3fe42ee18470a3f707efd499ecd05a99e727ff6c8a" +serialized_bytes_length = 5955737 +[[entries]] +file_name = "org.scala-lang_scala-reflect_2.13.6.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + + +[entries.coord] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" +[entries.file_digest] +fingerprint = "f713593809b387c60935bb9a940dfcea53bd0dbf8fdc8d10739a2896f8ac56fa" +serialized_bytes_length = 3769997 +[[entries]] +file_name = "org.scalactic_scalactic_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + + +[entries.coord] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "73a094a12ad95e2aff2b4861e1b8dcf78a724e3979ee3eb9bafdab878c79199e" +serialized_bytes_length = 1571937 +[[entries]] +directDependencies = [] +dependencies = [] +file_name = "org.scalatest_scalatest-compatible_3.2.10.jar" + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "fbc803990b400f41f48fb64d169a66eb5645a667e05a6d73d835a8cbe6c31c06" +serialized_bytes_length = 1413 +[[entries]] +file_name = "org.scalatest_scalatest-core_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "b760c723768346792d7a041b9bdf3f61b58bef34f996ebe1b7f1318d8da1ea96" +serialized_bytes_length = 3757804 +[[entries]] +file_name = "org.scalatest_scalatest-diagrams_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-diagrams_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "a31d872c4e18cd4c2a0e1af0318c8b2f5837e28553cca88e95260b1d8fabd77b" +serialized_bytes_length = 51341 +[[entries]] +file_name = "org.scalatest_scalatest-featurespec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-featurespec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "04927c2a5507c4d64944be91ac2376f51600a35f805d054c68b4b6c46ff86f01" +serialized_bytes_length = 81788 +[[entries]] +file_name = "org.scalatest_scalatest-flatspec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-flatspec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "7657ee10a6e8b03e96835988438f1c0901968b089657cf124f57abdb7bc7c20c" +serialized_bytes_length = 177437 +[[entries]] +file_name = "org.scalatest_scalatest-freespec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-freespec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "822b3a9a1e2189b6a3e4a43f87b9627d446f764d3b86d0e93f21e0bf7fb3c229" +serialized_bytes_length = 104003 +[[entries]] +file_name = "org.scalatest_scalatest-funspec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-funspec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "b77764eb1149d76ad6ba671d2d27140a35d1c6f2055e8c946fcc02ad7490e08d" +serialized_bytes_length = 110559 +[[entries]] +file_name = "org.scalatest_scalatest-funsuite_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-funsuite_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "e948d33c6526c0da3bf5cc9b0bb127dad76ce82d0edf1220b2c6b7cff23a5a69" +serialized_bytes_length = 64568 +[[entries]] +file_name = "org.scalatest_scalatest-matchers-core_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "6a23c628c711f257cc4f346429cacc463dd065f8b0039ad5f063cba0b6db8518" +serialized_bytes_length = 2033840 +[[entries]] +file_name = "org.scalatest_scalatest-mustmatchers_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-mustmatchers_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "f994a1b0e9cb77beb0174bf171083b502125507989c3efcfb2d29a8476041bbf" +serialized_bytes_length = 183996 +[[entries]] +file_name = "org.scalatest_scalatest-propspec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-propspec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "621fb80fb18b58e3fa44691f6d2e5606d5aea2d471433e94f5910dea200ae86f" +serialized_bytes_length = 34713 +[[entries]] +file_name = "org.scalatest_scalatest-refspec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-refspec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "c850db9983aed1c541d7489ec6a316e2abdaa88287cd85a747f92116ee3f8b7f" +serialized_bytes_length = 26565 +[[entries]] +file_name = "org.scalatest_scalatest-shouldmatchers_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-shouldmatchers_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "175266bf78309ac99d58f571d17b0505b30d0a638912248965e0699a1376405d" +serialized_bytes_length = 185176 +[[entries]] +file_name = "org.scalatest_scalatest-wordspec_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest-wordspec_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "08f983b45ef2306eaca35e9280aae2b8691a063e1f2d04b0f5d38f910cda7c19" +serialized_bytes_length = 134857 +[[entries]] +file_name = "org.scalatest_scalatest_2.13_3.2.10.jar" +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-diagrams_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-flatspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-funspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-refspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-wordspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-propspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-shouldmatchers_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-mustmatchers_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-freespec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-funsuite_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.directDependencies]] +group = "org.scalatest" +artifact = "scalatest-featurespec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-diagrams_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-flatspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-funspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-reflect" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang" +artifact = "scala-library" +version = "2.13.6" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-refspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-wordspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scala-lang.modules" +artifact = "scala-xml_2.13" +version = "1.3.0" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-propspec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-compatible" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-shouldmatchers_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-mustmatchers_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-freespec_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-funsuite_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalactic" +artifact = "scalactic_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-matchers-core_2.13" +version = "3.2.10" +packaging = "jar" + +[[entries.dependencies]] +group = "org.scalatest" +artifact = "scalatest-featurespec_2.13" +version = "3.2.10" +packaging = "jar" + + +[entries.coord] +group = "org.scalatest" +artifact = "scalatest_2.13" +version = "3.2.10" +packaging = "jar" +[entries.file_digest] +fingerprint = "5cdde7f43bd4e287cd9302c2a80c5badc82cd442b392d6aaf28f16b4d6f1321c" +serialized_bytes_length = 720 diff --git a/src/python/pants/backend/scala/test/scalatest_test.py b/src/python/pants/backend/scala/test/scalatest_test.py index 9480be0f0f65..e7d48dd58073 100644 --- a/src/python/pants/backend/scala/test/scalatest_test.py +++ b/src/python/pants/backend/scala/test/scalatest_test.py @@ -3,11 +3,15 @@ from __future__ import annotations -import importlib.resources from textwrap import dedent +from typing import Iterable, Mapping import pytest +from internal_plugins.test_lockfile_fixtures.lockfile_fixture import ( + JVMLockfileFixture, + JVMLockfileFixtureDefinition, +) from pants.backend.scala.compile.scalac import rules as scalac_rules from pants.backend.scala.subsystems.scalatest import Scalatest from pants.backend.scala.target_types import ( @@ -19,7 +23,7 @@ from pants.backend.scala.test.scalatest import ScalatestTestFieldSet from pants.backend.scala.test.scalatest import rules as scalatest_rules from pants.build_graph.address import Address -from pants.core.goals.test import TestResult +from pants.core.goals.test import TestResult, get_filtered_environment from pants.core.target_types import FilesGeneratorTarget, FileTarget, RelocatedFiles from pants.core.util_rules import config_files, source_files, system_binaries from pants.engine.addresses import Addresses @@ -27,7 +31,6 @@ from pants.jvm import classpath from pants.jvm.jdk_rules import rules as jdk_util_rules from pants.jvm.non_jvm_dependencies import rules as non_jvm_dependencies_rules -from pants.jvm.resolve.common import Coordinate from pants.jvm.resolve.coursier_fetch import rules as coursier_fetch_rules from pants.jvm.resolve.coursier_setup import rules as coursier_setup_rules from pants.jvm.strip_jar import strip_jar @@ -59,6 +62,7 @@ def rule_runner() -> RuleRunner: *system_binaries.rules(), *target_types_rules(), *util_rules(), + get_filtered_environment, QueryRule(CoarsenedTargets, (Addresses,)), QueryRule(TestResult, (ScalatestTestFieldSet,)), QueryRule(Scalatest, ()), @@ -72,31 +76,35 @@ def rule_runner() -> RuleRunner: ScalatestTestsGeneratorTarget, ], ) - rule_runner.set_options(args=[], env_inherit=PYTHON_BOOTSTRAP_ENV) return rule_runner +@pytest.fixture +def scalatest_lockfile_def() -> JVMLockfileFixtureDefinition: + return JVMLockfileFixtureDefinition( + "scalatest.test.lock", ["org.scalatest:scalatest_2.13:3.2.10"] + ) + + +@pytest.fixture +def scalatest_lockfile( + scalatest_lockfile_def: JVMLockfileFixtureDefinition, request +) -> JVMLockfileFixture: + return scalatest_lockfile_def.load(request) + + @maybe_skip_jdk_test -def test_simple_success(rule_runner: RuleRunner) -> None: - scalatest_coord = Coordinate(group="org.scalatest", artifact="scalatest_2.13", version="3.2.10") +def test_simple_success(rule_runner: RuleRunner, scalatest_lockfile: JVMLockfileFixture) -> None: rule_runner.write_files( { - "3rdparty/jvm/default.lock": importlib.resources.read_text( - *Scalatest.default_lockfile_resource - ), + "3rdparty/jvm/default.lock": scalatest_lockfile.serialized_lockfile, + "3rdparty/jvm/BUILD": scalatest_lockfile.requirements_as_jvm_artifact_targets(), "BUILD": dedent( - f"""\ - jvm_artifact( - name = 'org.scalatest_scalatest', - group = '{scalatest_coord.group}', - artifact = '{scalatest_coord.artifact}', - version = '{scalatest_coord.version}', - ) - + """\ scalatest_tests( name='example-test', dependencies= [ - ':org.scalatest_scalatest', + '3rdparty/jvm:org.scalatest_scalatest_2.13', ], ) """ @@ -127,22 +135,13 @@ class SimpleSpec extends AnyFunSpec { @maybe_skip_jdk_test -def test_file_deps_success(rule_runner: RuleRunner) -> None: - scalatest_coord = Coordinate(group="org.scalatest", artifact="scalatest_2.13", version="3.2.10") +def test_file_deps_success(rule_runner: RuleRunner, scalatest_lockfile: JVMLockfileFixture) -> None: rule_runner.write_files( { - "3rdparty/jvm/default.lock": importlib.resources.read_text( - *Scalatest.default_lockfile_resource - ), + "3rdparty/jvm/default.lock": scalatest_lockfile.serialized_lockfile, + "3rdparty/jvm/BUILD": scalatest_lockfile.requirements_as_jvm_artifact_targets(), "BUILD": dedent( - f"""\ - jvm_artifact( - name = 'org.scalatest_scalatest', - group = '{scalatest_coord.group}', - artifact = '{scalatest_coord.artifact}', - version = '{scalatest_coord.version}', - ) - + """\ scala_sources( name='example-sources', dependencies=[':ducks'], @@ -151,7 +150,7 @@ def test_file_deps_success(rule_runner: RuleRunner) -> None: scalatest_tests( name='example-test', dependencies= [ - ':org.scalatest_scalatest', + '3rdparty/jvm:org.scalatest_scalatest_2.13', ':example-sources', ], ) @@ -206,9 +205,74 @@ class SimpleSpec extends AnyFunSpec { assert test_result.xml_results and test_result.xml_results.files +@maybe_skip_jdk_test +def test_extra_env_vars(rule_runner: RuleRunner, scalatest_lockfile: JVMLockfileFixture) -> None: + rule_runner.write_files( + { + "3rdparty/jvm/default.lock": scalatest_lockfile.serialized_lockfile, + "3rdparty/jvm/BUILD": scalatest_lockfile.requirements_as_jvm_artifact_targets(), + "BUILD": dedent( + """\ + scalatest_tests( + name='example-test', + dependencies= [ + '3rdparty/jvm:org.scalatest_scalatest_2.13', + ], + extra_env_vars=[ + "SCALATEST_TESTS_VAR_WITHOUT_VALUE", + "SCALATEST_TESTS_VAR_WITH_VALUE=scalatest_tests_var_with_value", + "SCALATEST_TESTS_OVERRIDE_WITH_VALUE_VAR=scalatest_tests_override_with_value_var_override", + ], + ) + """ + ), + "ExtraEnvVarsSpec.scala": dedent( + """ + package org.pantsbuild.example + + import org.scalatest.flatspec.AnyFlatSpec + import org.scalatest.matchers.should.Matchers + + class ExtraEnvVarsSpec extends AnyFlatSpec with Matchers { + "ExtraEnvVars" should "be readable from the test" in { + sys.env("ARG_WITH_VALUE_VAR") shouldBe "arg_with_value_var" + sys.env("ARG_WITHOUT_VALUE_VAR") shouldBe "arg_without_value_var" + sys.env("SCALATEST_TESTS_VAR_WITH_VALUE") shouldBe "scalatest_tests_var_with_value" + sys.env("SCALATEST_TESTS_VAR_WITHOUT_VALUE") shouldBe "scalatest_tests_var_without_value" + sys.env("SCALATEST_TESTS_OVERRIDE_WITH_VALUE_VAR") shouldBe "scalatest_tests_override_with_value_var_override" + } + } + """ + ), + } + ) + + test_result = run_scalatest_test( + rule_runner, + "example-test", + "ExtraEnvVarsSpec.scala", + extra_args=[ + '--test-extra-env-vars=["ARG_WITH_VALUE_VAR=arg_with_value_var", "ARG_WITHOUT_VALUE_VAR", "SCALATEST_TESTS_OVERRIDE_WITH_VALUE_VAR"]' + ], + env={ + "ARG_WITHOUT_VALUE_VAR": "arg_without_value_var", + "SCALATEST_TESTS_VAR_WITHOUT_VALUE": "scalatest_tests_var_without_value", + "SCALATEST_TESTS_OVERRIDE_WITH_VALUE_VAR": "scalatest_tests_override_with_value_var", + }, + ) + assert test_result.exit_code == 0 + + def run_scalatest_test( - rule_runner: RuleRunner, target_name: str, relative_file_path: str + rule_runner: RuleRunner, + target_name: str, + relative_file_path: str, + *, + extra_args: Iterable[str] | None = None, + env: Mapping[str, str] | None = None, ) -> TestResult: + args = extra_args or [] + rule_runner.set_options(args, env=env, env_inherit=PYTHON_BOOTSTRAP_ENV) tgt = rule_runner.get_target( Address(spec_path="", target_name=target_name, relative_file_path=relative_file_path) ) diff --git a/src/python/pants/core/goals/test.py b/src/python/pants/core/goals/test.py index de74123d5a25..933ae1962ae9 100644 --- a/src/python/pants/core/goals/test.py +++ b/src/python/pants/core/goals/test.py @@ -38,6 +38,7 @@ NoApplicableTargetsBehavior, SourcesField, SpecialCasedDependencies, + StringSequenceField, TargetRootsToFieldSets, TargetRootsToFieldSetsRequest, Targets, @@ -463,6 +464,20 @@ def calculate_from_global_options(self, test: TestSubsystem) -> Optional[int]: return result +class TestExtraEnvVarsField(StringSequenceField, metaclass=ABCMeta): + alias = "extra_env_vars" + help = softwrap( + """ + Additional environment variables to include in test processes. + + Entries are strings in the form `ENV_VAR=value` to use explicitly; or just + `ENV_VAR` to copy the value of a variable in Pants's own environment. + + This will be merged with and override values from `[test].extra_env_vars`. + """ + ) + + @rule_helper async def _run_debug_tests( test_subsystem: TestSubsystem, diff --git a/src/python/pants/jvm/target_types.py b/src/python/pants/jvm/target_types.py index 4f9f4f712136..c8ce68d44aaf 100644 --- a/src/python/pants/jvm/target_types.py +++ b/src/python/pants/jvm/target_types.py @@ -9,7 +9,7 @@ from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError from pants.core.goals.package import OutputPathField from pants.core.goals.run import RestartableField -from pants.core.goals.test import TestTimeoutField +from pants.core.goals.test import TestExtraEnvVarsField, TestTimeoutField from pants.engine.addresses import Address from pants.engine.rules import collect_rules, rule from pants.engine.target import ( @@ -313,6 +313,10 @@ class JunitTestTimeoutField(TestTimeoutField): pass +class JunitTestExtraEnvVarsField(TestExtraEnvVarsField): + pass + + # ----------------------------------------------------------------------------------------------- # JAR support fields # ----------------------------------------------------------------------------------------------- diff --git a/src/python/pants/jvm/test/junit.py b/src/python/pants/jvm/test/junit.py index 8fd7a05578ec..fa867007eca2 100644 --- a/src/python/pants/jvm/test/junit.py +++ b/src/python/pants/jvm/test/junit.py @@ -11,6 +11,8 @@ from pants.core.goals.test import ( TestDebugAdapterRequest, TestDebugRequest, + TestExtraEnv, + TestExtraEnvVarsField, TestFieldSet, TestResult, TestSubsystem, @@ -18,6 +20,7 @@ from pants.core.target_types import FileSourceField from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest from pants.engine.addresses import Addresses +from pants.engine.environment import Environment, EnvironmentRequest from pants.engine.fs import Digest, DigestSubset, MergeDigests, PathGlobs, RemovePrefix, Snapshot from pants.engine.process import ( FallibleProcessResult, @@ -57,6 +60,7 @@ class JunitTestFieldSet(TestFieldSet): timeout: JunitTestTimeoutField jdk_version: JvmJdkField dependencies: JvmDependenciesField + extra_env_vars: TestExtraEnvVarsField class JunitToolLockfileSentinel(GenerateJvmToolLockfileSentinel): @@ -81,6 +85,7 @@ async def setup_junit_for_target( jvm: JvmSubsystem, junit: JUnit, test_subsystem: TestSubsystem, + test_extra_env: TestExtraEnv, ) -> TestSetup: jdk, transitive_tgts = await MultiGet( @@ -124,6 +129,10 @@ async def setup_junit_for_target( if request.is_debug: extra_jvm_args.extend(jvm.debug_args) + field_set_extra_env = await Get( + Environment, EnvironmentRequest(request.field_set.extra_env_vars.value or ()) + ) + process = JvmProcess( jdk=jdk, classpath_entries=[ @@ -140,6 +149,7 @@ async def setup_junit_for_target( *junit.args, ], input_digest=input_digest, + extra_env={**test_extra_env.env, **field_set_extra_env}, extra_jvm_options=junit.jvm_options, extra_immutable_input_digests=extra_immutable_input_digests, output_directories=(reports_dir,), diff --git a/src/python/pants/jvm/test/junit_test.py b/src/python/pants/jvm/test/junit_test.py index 54a6e7a35042..f5631ff4e226 100644 --- a/src/python/pants/jvm/test/junit_test.py +++ b/src/python/pants/jvm/test/junit_test.py @@ -5,7 +5,7 @@ import re from textwrap import dedent -from typing import Iterable +from typing import Iterable, Mapping import pytest @@ -20,7 +20,7 @@ from pants.backend.scala.target_types import ScalaJunitTestsGeneratorTarget from pants.backend.scala.target_types import rules as scala_target_types_rules from pants.build_graph.address import Address -from pants.core.goals.test import TestResult +from pants.core.goals.test import TestResult, get_filtered_environment from pants.core.target_types import FilesGeneratorTarget, FileTarget, RelocatedFiles from pants.core.util_rules import config_files, source_files from pants.core.util_rules.external_tool import rules as external_tool_rules @@ -62,6 +62,7 @@ def rule_runner() -> RuleRunner: *target_types_rules(), *util_rules(), *non_jvm_dependencies_rules(), + get_filtered_environment, QueryRule(CoarsenedTargets, (Addresses,)), QueryRule(TestResult, (JunitTestFieldSet,)), ], @@ -75,11 +76,6 @@ def rule_runner() -> RuleRunner: ScalaJunitTestsGeneratorTarget, ], ) - rule_runner.set_options( - # Makes JUnit output predictable and parseable across versions (#12933): - args=["--junit-args=['--disable-ansi-colors','--details=flat','--details-theme=ascii']"], - env_inherit=PYTHON_BOOTSTRAP_ENV, - ) return rule_runner @@ -572,9 +568,75 @@ def test_vintage_relocated_files_dependency( assert re.search(r"1 tests found", test_result.stdout) is not None +@maybe_skip_jdk_test +def test_vintage_extra_env_vars( + rule_runner: RuleRunner, junit4_lockfile: JVMLockfileFixture +) -> None: + rule_runner.write_files( + { + "3rdparty/jvm/default.lock": junit4_lockfile.serialized_lockfile, + "3rdparty/jvm/BUILD": junit4_lockfile.requirements_as_jvm_artifact_targets(), + "BUILD": dedent( + """\ + junit_tests( + name="example-test", + extra_env_vars=[ + "JUNIT_TESTS_VAR_WITHOUT_VALUE", + "JUNIT_TESTS_VAR_WITH_VALUE=junit_tests_var_with_value", + "JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR=junit_tests_override_with_value_var_override", + ], + ) + """ + ), + "ExtraEnvVarsTest.java": dedent( + """\ + package org.pantsbuild.example; + + import junit.framework.TestCase; + + public class ExtraEnvVarsTest extends TestCase { + public void testArgs() throws Exception { + assertEquals(System.getenv("ARG_WITH_VALUE_VAR"), "arg_with_value_var"); + assertEquals(System.getenv("ARG_WITHOUT_VALUE_VAR"), "arg_without_value_var"); + assertEquals(System.getenv("JUNIT_TESTS_VAR_WITH_VALUE"), "junit_tests_var_with_value"); + assertEquals(System.getenv("JUNIT_TESTS_VAR_WITHOUT_VALUE"), "junit_tests_var_without_value"); + assertEquals(System.getenv("JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR"), "junit_tests_override_with_value_var_override"); + } + } + """ + ), + } + ) + + result = run_junit_test( + rule_runner, + "example-test", + "ExtraEnvVarsTest.java", + extra_args=[ + '--test-extra-env-vars=["ARG_WITH_VALUE_VAR=arg_with_value_var", "ARG_WITHOUT_VALUE_VAR", "JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR"]' + ], + env={ + "ARG_WITHOUT_VALUE_VAR": "arg_without_value_var", + "JUNIT_TESTS_VAR_WITHOUT_VALUE": "junit_tests_var_without_value", + "JUNIT_TESTS_OVERRIDE_WITH_VALUE_VAR": "junit_tests_override_with_value_var", + }, + ) + assert result.exit_code == 0 + + def run_junit_test( - rule_runner: RuleRunner, target_name: str, relative_file_path: str + rule_runner: RuleRunner, + target_name: str, + relative_file_path: str, + *, + extra_args: Iterable[str] | None = None, + env: Mapping[str, str] | None = None, ) -> TestResult: + args = [ + "--junit-args=['--disable-ansi-colors','--details=flat','--details-theme=ascii']", + *(extra_args or ()), + ] + rule_runner.set_options(args, env=env, env_inherit=PYTHON_BOOTSTRAP_ENV) tgt = rule_runner.get_target( Address(spec_path="", target_name=target_name, relative_file_path=relative_file_path) )