| // Copyright 2015 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.analysis; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.devtools.build.lib.analysis.testing.ExecGroupCollectionSubject.assertThat; |
| import static com.google.devtools.build.lib.packages.ExecGroup.DEFAULT_EXEC_GROUP_NAME; |
| |
| import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue; |
| import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.packages.Provider; |
| import com.google.devtools.build.lib.packages.StarlarkProvider; |
| import com.google.devtools.build.lib.packages.StructImpl; |
| import java.io.IOException; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Test for exec groups. Functionality related to rule context tested in {@link |
| * com.google.devtools.build.lib.starlark.StarlarkRuleContextTest}. |
| */ |
| @RunWith(JUnit4.class) |
| public class StarlarkExecGroupTest extends BuildViewTestCase { |
| /** |
| * Sets up two toolchains types, each with a single toolchain implementation and a single |
| * exec_compatible_with platform. |
| * |
| * <p>toolchain_type_1 -> foo_toolchain -> exec_compatible_with platform_1 toolchain_type_2 -> |
| * bar_toolchain -> exec_compatible_with platform_2 |
| */ |
| private void createToolchainsAndPlatforms() throws Exception { |
| scratch.file( |
| "rule/test_toolchain.bzl", |
| "def _impl(ctx):", |
| " return [platform_common.ToolchainInfo()]", |
| "test_toolchain = rule(", |
| " implementation = _impl,", |
| ")"); |
| scratch.file( |
| "rule/BUILD", |
| "exports_files(['test_toolchain/bzl'])", |
| "toolchain_type(name = 'toolchain_type_1')", |
| "toolchain_type(name = 'toolchain_type_2')"); |
| scratch.file( |
| "toolchain/BUILD", |
| "load('//rule:test_toolchain.bzl', 'test_toolchain')", |
| "test_toolchain(", |
| " name = 'foo',", |
| ")", |
| "toolchain(", |
| " name = 'foo_toolchain',", |
| " toolchain_type = '//rule:toolchain_type_1',", |
| " target_compatible_with = ['//platform:constraint_1'],", |
| " exec_compatible_with = ['//platform:constraint_1'],", |
| " toolchain = ':foo',", |
| ")", |
| "test_toolchain(", |
| " name = 'bar',", |
| ")", |
| "toolchain(", |
| " name = 'bar_toolchain',", |
| " toolchain_type = '//rule:toolchain_type_2',", |
| " target_compatible_with = ['//platform:constraint_1'],", |
| " exec_compatible_with = ['//platform:constraint_2'],", |
| " toolchain = ':bar',", |
| ")"); |
| |
| scratch.overwriteFile( |
| "platform/BUILD", |
| "constraint_setting(name = 'setting')", |
| "constraint_value(", |
| " name = 'constraint_1',", |
| " constraint_setting = ':setting',", |
| ")", |
| "constraint_value(", |
| " name = 'constraint_2',", |
| " constraint_setting = ':setting',", |
| ")", |
| "platform(", |
| " name = 'platform_1',", |
| " constraint_values = [':constraint_1'],", |
| ")", |
| "platform(", |
| " name = 'platform_2',", |
| " constraint_values = [':constraint_2'],", |
| " exec_properties = {", |
| " 'watermelon.ripeness': 'unripe',", |
| " 'watermelon.color': 'red',", |
| " },", |
| ")"); |
| |
| useConfiguration( |
| "--extra_toolchains=//toolchain:foo_toolchain,//toolchain:bar_toolchain", |
| "--platforms=//platform:platform_1", |
| "--extra_execution_platforms=//platform:platform_1,//platform:platform_2"); |
| } |
| |
| @Test |
| public void testDirectExecTransitionWithToolchains() throws Exception { |
| // toolchain_2 is available on platform_2, so exec transition also needs to be to platform_2 |
| createToolchainsAndPlatforms(); |
| |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl(ctx):", |
| " return [MyInfo(dep = ctx.attr.dep)]", |
| "with_transition = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'dep': attr.label(cfg = 'exec'),", |
| " },", |
| " toolchains = ['//rule:toolchain_type_2'],", |
| ")", |
| "def _impl2(ctx):", |
| " return []", |
| "simple_rule = rule(implementation = _impl2)"); |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_transition', 'simple_rule')", |
| "with_transition(name = 'parent', dep = ':child')", |
| "simple_rule(name = 'child')"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:parent"); |
| Provider.Key key = new StarlarkProvider.Key(Label.parseCanonical("//test:defs.bzl"), "MyInfo"); |
| BuildConfigurationValue dep = |
| getConfiguration((ConfiguredTarget) ((StructImpl) target.get(key)).getValue("dep")); |
| |
| assertThat(dep.getFragment(PlatformConfiguration.class).getTargetPlatform()) |
| .isEqualTo(Label.parseCanonicalUnchecked("//platform:platform_2")); |
| } |
| |
| @Test |
| public void testIndirectExecTransitionWithToolchains() throws Exception { |
| createToolchainsAndPlatforms(); |
| useConfiguration( |
| "--extra_toolchains=//toolchain:foo_toolchain,//toolchain:bar_toolchain", |
| "--platforms=//platform:platform_1", |
| "--extra_execution_platforms=//platform:platform_1,//platform:platform_2", |
| "--incompatible_auto_exec_groups"); |
| |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl_parent(ctx):", |
| " output = ctx.actions.declare_file('parent.out')", |
| " ctx.actions.run(", |
| " executable = '',", |
| " progress_message = 'Test with AEG.',", |
| " outputs = [output],", |
| " )", |
| " return [MyInfo(dep = ctx.attr.dep), DefaultInfo(files = depset([output]))]", |
| "parent_rule = rule(", |
| " implementation = _impl_parent,", |
| " attrs = {", |
| " 'dep': attr.label(),", |
| " '_use_auto_exec_groups': attr.bool(default = True),", |
| " },", |
| " toolchains = ['//rule:toolchain_type_2'],", |
| ")", |
| "def _impl(ctx):", |
| " return [MyInfo(dep = ctx.attr.dep)]", |
| "pass_thru = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'dep': attr.label(cfg = 'exec'),", |
| " },", |
| ")", |
| "def _impl2(ctx):", |
| " return []", |
| "simple_rule = rule(implementation = _impl2)"); |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'parent_rule', 'pass_thru', 'simple_rule')", |
| "parent_rule(name = 'parent', dep = ':passthru')", |
| "pass_thru(name = 'passthru', dep = ':child')", |
| "simple_rule(name = 'child')"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:parent"); |
| Provider.Key key = new StarlarkProvider.Key(Label.parseCanonical("//test:defs.bzl"), "MyInfo"); |
| ConfiguredTarget dep = (ConfiguredTarget) ((StructImpl) target.get(key)).getValue("dep"); |
| BuildConfigurationValue passthruDepConfig = |
| getConfiguration((ConfiguredTarget) ((StructImpl) dep.get(key)).getValue("dep")); |
| |
| // Action will be executed on '//platform:platform_1' plaform. |
| assertThat( |
| getGeneratingAction(target, "test/parent.out") |
| .getOwner() |
| .getExecutionPlatform() |
| .label()) |
| .isEqualTo(passthruDepConfig.getFragment(PlatformConfiguration.class).getTargetPlatform()); |
| } |
| |
| @Test |
| public void testExecGroupTransition() throws Exception { |
| createToolchainsAndPlatforms(); |
| |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl(ctx):", |
| " return [MyInfo(dep = ctx.attr.dep, exec_group_dep = ctx.attr.exec_group_dep)]", |
| "with_transition = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'exec_group_dep': attr.label(cfg = config.exec('watermelon')),", |
| " 'dep': attr.label(cfg = 'exec'),", |
| " },", |
| " exec_groups = {", |
| " 'watermelon': exec_group(toolchains = ['//rule:toolchain_type_2']),", |
| " },", |
| " toolchains = ['//rule:toolchain_type_1'],", |
| ")", |
| "def _impl2(ctx):", |
| " return []", |
| "simple_rule = rule(implementation = _impl2)"); |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_transition', 'simple_rule')", |
| "with_transition(name = 'parent', dep = ':child', exec_group_dep = ':other-child')", |
| "simple_rule(name = 'child')", |
| "simple_rule(name = 'other-child')"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:parent"); |
| Provider.Key key = new StarlarkProvider.Key(Label.parseCanonical("//test:defs.bzl"), "MyInfo"); |
| BuildConfigurationValue dep = |
| getConfiguration((ConfiguredTarget) ((StructImpl) target.get(key)).getValue("dep")); |
| BuildConfigurationValue execGroupDep = |
| getConfiguration( |
| (ConfiguredTarget) ((StructImpl) target.get(key)).getValue("exec_group_dep")); |
| |
| assertThat(dep.getFragment(PlatformConfiguration.class).getTargetPlatform()) |
| .isEqualTo(Label.parseCanonicalUnchecked("//platform:platform_1")); |
| assertThat(execGroupDep.getFragment(PlatformConfiguration.class).getTargetPlatform()) |
| .isEqualTo(Label.parseCanonicalUnchecked("//platform:platform_2")); |
| } |
| |
| @Test |
| public void testInvalidExecGroupTransition() throws Exception { |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl(ctx):", |
| " return []", |
| "with_transition = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'exec_group_dep': attr.label(cfg = config.exec('blueberry')),", |
| " },", |
| ")", |
| "def _impl2(ctx):", |
| " return []", |
| "simple_rule = rule(implementation = _impl2)"); |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_transition', 'simple_rule')", |
| "with_transition(name = 'parent', exec_group_dep = ':child')", |
| "simple_rule(name = 'child')"); |
| |
| reporter.removeHandler(failFastHandler); |
| getConfiguredTarget("//test:parent"); |
| assertContainsEvent( |
| "Attr 'exec_group_dep' declares a transition for non-existent exec group 'blueberry'"); |
| } |
| |
| @Test |
| public void testExecGroupActionHasExecGroupPlatform() throws Exception { |
| createToolchainsAndPlatforms(); |
| writeRuleWithActionsAndWatermelonExecGroup(); |
| |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " output = 'out.txt',", |
| " watermelon_output = 'watermelon_out.txt'", |
| ")"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:papaya"); |
| |
| assertThat( |
| getGeneratingAction(target, "test/watermelon_out.txt") |
| .getOwner() |
| .getExecutionPlatform() |
| .label()) |
| .isEqualTo(Label.parseCanonicalUnchecked("//platform:platform_2")); |
| assertThat( |
| getGeneratingAction(target, "test/out.txt").getOwner().getExecutionPlatform().label()) |
| .isEqualTo(Label.parseCanonicalUnchecked("//platform:platform_1")); |
| } |
| |
| @Test |
| public void testActionDeclaresInvalidExecGroup() throws Exception { |
| createToolchainsAndPlatforms(); |
| |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl(ctx):", |
| " watermelon_out_file = ctx.outputs.watermelon_output", |
| " ctx.actions.run_shell(", |
| " inputs = [],", |
| " outputs = [watermelon_out_file],", |
| " arguments = [watermelon_out_file.path],", |
| " command = 'echo hello > \"$1\"',", |
| " exec_group = 'honeydew',", |
| " )", |
| "with_actions = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'watermelon_output': attr.output(),", |
| " },", |
| " exec_groups = {", |
| " 'watermelon': exec_group(toolchains = ['//rule:toolchain_type_2']),", |
| " },", |
| ")"); |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " watermelon_output = 'watermelon_out.txt'", |
| ")"); |
| |
| reporter.removeHandler(failFastHandler); |
| getConfiguredTarget("//test:papaya"); |
| assertContainsEvent("Action declared for non-existent exec group 'honeydew'"); |
| } |
| |
| @Test |
| public void ruleCannotNameExecGroupDefaultName() throws Exception { |
| createToolchainsAndPlatforms(); |
| |
| scratch.file( |
| "test/defs.bzl", |
| "def _impl(ctx):", |
| " return []", |
| "my_rule = rule(", |
| " implementation = _impl,", |
| " exec_groups = {", |
| " '" |
| + DEFAULT_EXEC_GROUP_NAME |
| + "': exec_group(toolchains = ['//rule:toolchain_type_2']),", |
| " },", |
| ")"); |
| scratch.file( |
| "test/BUILD", // |
| "load('//test:defs.bzl', 'my_rule')", |
| "my_rule(name = 'papaya')"); |
| |
| reporter.removeHandler(failFastHandler); |
| getConfiguredTarget("//test:papaya"); |
| assertContainsEvent("Exec group name '" + DEFAULT_EXEC_GROUP_NAME + "' is not a valid name"); |
| } |
| |
| private void createAspectRuleWithExecGroup(String execGroupName) throws IOException { |
| scratch.file( |
| "test/defs.bzl", |
| "def _aspect_impl(target, ctx):", |
| " return []", |
| "my_aspect = aspect(", |
| " implementation = _aspect_impl,", |
| " exec_groups = {", |
| " '" + execGroupName + "': exec_group(toolchains = ['//rule:toolchain_type_2']),", |
| " },", |
| " toolchains = ['//rule:toolchain_type_1'],", |
| ")", |
| "def _rule_impl(ctx):", |
| " return []", |
| "my_rule = rule(", |
| " implementation = _rule_impl,", |
| " attrs = {", |
| " 'srcs': attr.label_list(aspects = [my_aspect])", |
| " },", |
| ")"); |
| } |
| |
| @Test |
| public void aspectUsesExecGroup() throws Exception { |
| createToolchainsAndPlatforms(); |
| createAspectRuleWithExecGroup("watermelon"); |
| |
| scratch.file( |
| "test/BUILD", |
| "load(':defs.bzl', 'my_rule')", |
| "filegroup(name = 'banana')", |
| "my_rule(name = 'papaya', srcs = [':banana'])"); |
| |
| ConfiguredTarget configuredTarget = getConfiguredTarget("//test:papaya"); |
| assertThat(configuredTarget).isNotNull(); |
| } |
| |
| @Test |
| public void aspectCannotNameExecGroupDefaultName() throws Exception { |
| createToolchainsAndPlatforms(); |
| createAspectRuleWithExecGroup(DEFAULT_EXEC_GROUP_NAME); |
| |
| scratch.file( |
| "test/BUILD", |
| "load(':defs.bzl', 'my_rule')", |
| "filegroup(name = 'banana')", |
| "my_rule(name = 'papaya', srcs = [':banana'])"); |
| |
| reporter.removeHandler(failFastHandler); |
| getConfiguredTarget("//test:papaya"); |
| assertContainsEvent("Exec group name '" + DEFAULT_EXEC_GROUP_NAME + "' is not a valid name"); |
| } |
| |
| private void writeRuleWithActionsAndWatermelonExecGroup() throws Exception { |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl(ctx):", |
| " watermelon_out_file = ctx.outputs.watermelon_output", |
| " ctx.actions.run_shell(", |
| " inputs = [],", |
| " outputs = [watermelon_out_file],", |
| " arguments = [watermelon_out_file.path],", |
| " command = 'echo hello > \"$1\"',", |
| " exec_group = 'watermelon',", |
| " )", |
| " out_file = ctx.outputs.output", |
| " ctx.actions.run_shell(", |
| " inputs = [],", |
| " outputs = [out_file],", |
| " arguments = [out_file.path],", |
| " command = 'echo hello > \"$1\"',", |
| " )", |
| "with_actions = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'watermelon_output': attr.output(),", |
| " 'output': attr.output(),", |
| " },", |
| " exec_groups = {", |
| " 'watermelon': exec_group(toolchains = ['//rule:toolchain_type_2']),", |
| " },", |
| " toolchains = ['//rule:toolchain_type_1'],", |
| ")"); |
| } |
| |
| @Test |
| public void testSetExecGroupExecProperty() throws Exception { |
| createToolchainsAndPlatforms(); |
| writeRuleWithActionsAndWatermelonExecGroup(); |
| |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " output = 'out.txt',", |
| " watermelon_output = 'watermelon_out.txt',", |
| " exec_properties = {", |
| " 'color': 'orange',", |
| " 'ripeness': 'ripe',", |
| " 'watermelon.color': 'pink',", |
| " 'watermelon.season': 'summer',", |
| " },", |
| ")"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:papaya"); |
| |
| assertThat( |
| getGeneratingAction(target, "test/watermelon_out.txt").getOwner().getExecProperties()) |
| .containsExactly("color", "pink", "season", "summer", "ripeness", "ripe"); |
| assertThat(getGeneratingAction(target, "test/out.txt").getOwner().getExecProperties()) |
| .containsExactly("color", "orange", "ripeness", "ripe"); |
| } |
| |
| @Test |
| public void testSetUnknownExecGroup() throws Exception { |
| createToolchainsAndPlatforms(); |
| writeRuleWithActionsAndWatermelonExecGroup(); |
| |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " output = 'out.txt',", |
| " watermelon_output = 'watermelon_out.txt',", |
| " exec_properties = {", |
| " 'color': 'orange',", |
| " 'watermelon.color': 'pink',", |
| " 'blueberry.season': 'summer',", // non-existent exec group |
| " },", |
| ")"); |
| |
| reporter.removeHandler(failFastHandler); |
| getConfiguredTarget("//test:papaya"); |
| assertContainsEvent("errors encountered while analyzing target '//test:papaya'"); |
| } |
| |
| @Test |
| public void ruleInheritsRuleRequirements() throws Exception { |
| createToolchainsAndPlatforms(); |
| scratch.file( |
| "test/defs.bzl", |
| "MyInfo = provider()", |
| "def _impl(ctx):", |
| " return []", |
| "my_rule = rule(", |
| " implementation = _impl,", |
| " exec_groups = {", |
| " 'watermelon': exec_group(copy_from_rule = True),", |
| " },", |
| " exec_compatible_with = ['//platform:constraint_1'],", |
| " toolchains = ['//rule:toolchain_type_1'],", |
| ")"); |
| scratch.file("test/BUILD", "load('//test:defs.bzl', 'my_rule')", "my_rule(name = 'papaya')"); |
| |
| ConfiguredTarget ct = getConfiguredTarget("//test:papaya"); |
| ExecGroupCollection execGroups = getRuleContext(ct).getExecGroups(); |
| assertThat(execGroups).isNotNull(); |
| assertThat(execGroups).hasExecGroup("watermelon"); |
| // TODO(https://github.com/bazelbuild/bazel/issues/14726): Add tests of optional toolchains. |
| assertThat(execGroups).execGroup("watermelon").hasToolchainType("//rule:toolchain_type_1"); |
| assertThat(execGroups) |
| .execGroup("watermelon") |
| .toolchainType("//rule:toolchain_type_1") |
| .isMandatory(); |
| assertThat(execGroups).execGroup("watermelon").hasExecCompatibleWith("//platform:constraint_1"); |
| } |
| |
| @Test |
| public void ruleInheritsPlatformExecGroupExecProperty() throws Exception { |
| createToolchainsAndPlatforms(); |
| writeRuleWithActionsAndWatermelonExecGroup(); |
| |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " output = 'out.txt',", |
| " watermelon_output = 'watermelon_out.txt',", |
| ")"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:papaya"); |
| |
| assertThat( |
| getGeneratingAction(target, "test/watermelon_out.txt").getOwner().getExecProperties()) |
| .containsExactly("ripeness", "unripe", "color", "red"); |
| assertThat(getGeneratingAction(target, "test/out.txt").getOwner().getExecProperties()) |
| .containsExactly(); |
| } |
| |
| @Test |
| public void aspectInheritsPlatformExecGroupExecProperty() throws Exception { |
| createToolchainsAndPlatforms(); |
| writeRuleWithActionsAndWatermelonExecGroup(); |
| |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " output = 'out.txt',", |
| " watermelon_output = 'watermelon_out.txt',", |
| ")"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:papaya"); |
| |
| assertThat( |
| getGeneratingAction(target, "test/watermelon_out.txt").getOwner().getExecProperties()) |
| .containsExactly("ripeness", "unripe", "color", "red"); |
| assertThat(getGeneratingAction(target, "test/out.txt").getOwner().getExecProperties()) |
| .containsExactly(); |
| } |
| |
| @Test |
| public void ruleOverridePlatformExecGroupExecProperty() throws Exception { |
| createToolchainsAndPlatforms(); |
| writeRuleWithActionsAndWatermelonExecGroup(); |
| |
| scratch.file( |
| "test/BUILD", |
| "load('//test:defs.bzl', 'with_actions')", |
| "with_actions(", |
| " name = 'papaya',", |
| " output = 'out.txt',", |
| " watermelon_output = 'watermelon_out.txt',", |
| " exec_properties = {", |
| " 'watermelon.ripeness': 'ripe',", |
| " 'ripeness': 'unknown',", |
| " },", |
| ")"); |
| |
| ConfiguredTarget target = getConfiguredTarget("//test:papaya"); |
| |
| assertThat( |
| getGeneratingAction(target, "test/watermelon_out.txt").getOwner().getExecProperties()) |
| .containsExactly("ripeness", "ripe", "color", "red"); |
| assertThat(getGeneratingAction(target, "test/out.txt").getOwner().getExecProperties()) |
| .containsExactly("ripeness", "unknown"); |
| } |
| } |