Update C# FeatureSetDescriptor to use auto-generated source of truth.

PiperOrigin-RevId: 692879910
pull/19118/head
Protobuf Team Bot 3 months ago committed by Copybara-Service
parent 2fe8aaa158
commit 4d72a223e4
  1. 33
      csharp/BUILD.bazel
  2. 64
      csharp/src/Google.Protobuf.Test/Reflection/FeatureSetDescriptorTest.cs
  3. 83
      csharp/src/Google.Protobuf/Reflection/FeatureSetDescriptor.cs
  4. 17
      csharp/src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs.template
  5. 1
      regenerate_stale_files.sh

@ -5,6 +5,8 @@
load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix")
load("//build_defs:internal_shell.bzl", "inline_sh_test")
load("//conformance:defs.bzl", "conformance_test")
load("//editions:defaults.bzl", "compile_edition_defaults", "embed_edition_defaults")
load("//upb/cmake:build_defs.bzl", "staleness_test")
################################################################################
# Tests
@ -114,3 +116,34 @@ sh_binary(
srcs = ["build_release.sh"],
args = ["$(location build_release.sh)"],
)
################################################################################
# Generated edition defaults (and staleness test)
################################################################################
compile_edition_defaults(
name = "csharp_edition_defaults",
srcs = [
"//:descriptor_proto",
],
maximum_edition = "2023",
minimum_edition = "PROTO2",
)
# TODO Make bazel tests use this output instead of the checked-in one
embed_edition_defaults(
name = "embedded_csharp_edition_defaults_generate",
defaults = "csharp_edition_defaults",
encoding = "base64",
output = "generated/src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs",
placeholder = "DEFAULTS_VALUE",
template = "src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs.template",
)
staleness_test(
name = "generated_csharp_defaults_staleness_test",
outs = ["src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs"],
generated_pattern = "generated/%s",
tags = ["manual"],
target_files = ["src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs"],
)

@ -10,33 +10,59 @@
using Google.Protobuf.Reflection;
using NUnit.Framework;
using System;
using System.Linq;
using static Google.Protobuf.Reflection.FeatureSet.Types;
namespace Google.Protobuf.Test.Reflection;
public class FeatureSetDescriptorTest
{
// Canonical serialized form of the edition defaults, generated by embed_edition_defaults.
// TODO: Update this automatically.
private const string DefaultsBase64 =
"ChMY5gciDAgBEAIYAiADKAEwAioAChMY5wciDAgCEAEYASACKAEwASoAChMY6AciDAgBEAEYASACKAEwASoAIOYHKOgH";
// Just selectively test a couple of hard-coded examples. This isn't meant to be exhaustive,
// and we don't expect to add new tests for later editions unless there's a production code change.
[Test]
[TestCase(Edition.Proto2)]
[TestCase(Edition.Proto3)]
[TestCase(Edition._2023)]
public void DefaultsMatchCanonicalSerializedForm(Edition edition)
public void Proto2Defaults()
{
var canonicalDefaults = FeatureSetDefaults.Parser
.WithDiscardUnknownFields(true) // Discard language-specific extensions.
.ParseFrom(Convert.FromBase64String(DefaultsBase64));
var canonicalEditionDefaults = new FeatureSet();
canonicalEditionDefaults.MergeFrom(
canonicalDefaults.Defaults.Single(def => def.Edition == edition).FixedFeatures);
canonicalEditionDefaults.MergeFrom(
canonicalDefaults.Defaults.Single(def => def.Edition == edition).OverridableFeatures);
var candidateEditionDefaults = FeatureSetDescriptor.GetEditionDefaults(edition).Proto;
var expectedDefaults = new FeatureSet
{
EnumType = EnumType.Closed,
FieldPresence = FieldPresence.Explicit,
JsonFormat = JsonFormat.LegacyBestEffort,
MessageEncoding = MessageEncoding.LengthPrefixed,
RepeatedFieldEncoding = RepeatedFieldEncoding.Expanded,
Utf8Validation = Utf8Validation.None,
};
var actualDefaults = FeatureSetDescriptor.GetEditionDefaults(Edition.Proto2).Proto;
Assert.AreEqual(expectedDefaults, actualDefaults);
}
[Test]
public void Proto3Defaults()
{
var expectedDefaults = new FeatureSet
{
EnumType = EnumType.Open,
FieldPresence = FieldPresence.Implicit,
JsonFormat = JsonFormat.Allow,
MessageEncoding = MessageEncoding.LengthPrefixed,
RepeatedFieldEncoding = RepeatedFieldEncoding.Packed,
Utf8Validation = Utf8Validation.Verify,
};
var actualDefaults = FeatureSetDescriptor.GetEditionDefaults(Edition.Proto3).Proto;
Assert.AreEqual(expectedDefaults, actualDefaults);
}
[Test]
public void MaxSupportedEdition()
{
// This should be the last piece of code to be changed when updating the C# runtime to support
// a new edition. It should only be changed when you're sure that all the features in the new
// edition are supported. Just changing the configuration for feature set default generation
// will *advertise* that we support the new edition, but that isn't sufficient.
Edition maxSupportedEdition = Edition._2023;
Assert.AreEqual(canonicalEditionDefaults, candidateEditionDefaults);
// These lines should not need to be changed.
FeatureSetDescriptor.GetEditionDefaults(maxSupportedEdition);
Edition invalidEdition = (Edition) (maxSupportedEdition + 1);
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureSetDescriptor.GetEditionDefaults(invalidEdition));
}
}

@ -9,6 +9,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using static Google.Protobuf.Reflection.FeatureSet.Types;
namespace Google.Protobuf.Reflection;
@ -22,52 +24,55 @@ namespace Google.Protobuf.Reflection;
/// If either of those features are ever implemented in this runtime,
/// the feature settings will be exposed as properties in this class.
/// </remarks>
internal sealed class FeatureSetDescriptor
internal sealed partial class FeatureSetDescriptor
{
private static readonly ConcurrentDictionary<FeatureSet, FeatureSetDescriptor> cache = new();
// Note: this approach is deliberately chosen to circumvent bootstrapping issues.
// This can still be tested using the binary representation.
// TODO: Generate this code (as a partial class) from the binary representation.
private static readonly FeatureSetDescriptor edition2023Defaults = new FeatureSetDescriptor(
new FeatureSet
{
EnumType = EnumType.Open,
FieldPresence = FieldPresence.Explicit,
JsonFormat = JsonFormat.Allow,
MessageEncoding = MessageEncoding.LengthPrefixed,
RepeatedFieldEncoding = RepeatedFieldEncoding.Packed,
Utf8Validation = Utf8Validation.Verify,
});
private static readonly FeatureSetDescriptor proto2Defaults = new FeatureSetDescriptor(
new FeatureSet
private static readonly IReadOnlyDictionary<Edition, FeatureSetDescriptor> descriptorsByEdition = BuildEditionDefaults();
// Note: if the debugger is set to break within this code, various type initializers will fail
// as the debugger will try to call ToString() on messages, requiring descriptors to be accessed etc.
// There's a possible workaround of using a hard-coded bootstrapping FeatureSetDescriptor to be returned
// by GetEditionDefaults if descriptorsByEdition is null, but it's ugly and likely just pushes the problem
// elsewhere. Normal debugging sessions (where the initial bootstrapping code doesn't hit any breakpoints)
// do not cause any problems.
private static IReadOnlyDictionary<Edition, FeatureSetDescriptor> BuildEditionDefaults()
{
var featureSetDefaults = FeatureSetDefaults.Parser.ParseFrom(Convert.FromBase64String(DefaultsBase64));
var ret = new Dictionary<Edition, FeatureSetDescriptor>();
// Note: Enum.GetValues<TEnum> isn't available until .NET 5. It's not worth making this conditional
// based on that.
var supportedEditions = ((Edition[]) Enum.GetValues(typeof(Edition)))
.OrderBy(x => x)
.Where(e => e >= featureSetDefaults.MinimumEdition && e <= featureSetDefaults.MaximumEdition);
// We assume the embedded defaults will always contain "legacy".
var currentDescriptor = MaybeCreateDescriptor(Edition.Legacy);
foreach (var edition in supportedEditions)
{
EnumType = EnumType.Closed,
FieldPresence = FieldPresence.Explicit,
JsonFormat = JsonFormat.LegacyBestEffort,
MessageEncoding = MessageEncoding.LengthPrefixed,
RepeatedFieldEncoding = RepeatedFieldEncoding.Expanded,
Utf8Validation = Utf8Validation.None,
});
private static readonly FeatureSetDescriptor proto3Defaults = new FeatureSetDescriptor(
new FeatureSet
currentDescriptor = MaybeCreateDescriptor(edition) ?? currentDescriptor;
ret[edition] = currentDescriptor;
}
return ret;
FeatureSetDescriptor MaybeCreateDescriptor(Edition edition)
{
EnumType = EnumType.Open,
FieldPresence = FieldPresence.Implicit,
JsonFormat = JsonFormat.Allow,
MessageEncoding = MessageEncoding.LengthPrefixed,
RepeatedFieldEncoding = RepeatedFieldEncoding.Packed,
Utf8Validation = Utf8Validation.Verify,
});
var editionDefaults = featureSetDefaults.Defaults.SingleOrDefault(d => d.Edition == edition);
if (editionDefaults is null)
{
return null;
}
var proto = new FeatureSet();
proto.MergeFrom(editionDefaults.FixedFeatures);
proto.MergeFrom(editionDefaults.OverridableFeatures);
return new FeatureSetDescriptor(proto);
}
}
internal static FeatureSetDescriptor GetEditionDefaults(Edition edition) =>
edition switch
{
Edition.Proto2 => proto2Defaults,
Edition.Proto3 => proto3Defaults,
Edition._2023 => edition2023Defaults,
_ => throw new ArgumentOutOfRangeException($"Unsupported edition: {edition}")
};
descriptorsByEdition.TryGetValue(edition, out var defaults) ? defaults
: throw new ArgumentOutOfRangeException($"Unsupported edition: {edition}");
// Visible for testing. The underlying feature set proto, usually derived during
// feature resolution.

@ -0,0 +1,17 @@
#region Copyright notice and license
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#endregion
namespace Google.Protobuf.Reflection;
internal sealed partial class FeatureSetDescriptor
{
// Canonical serialized form of the edition defaults, generated by embed_edition_defaults.
private const string DefaultsBase64 =
"DEFAULTS_VALUE";
}

@ -13,6 +13,7 @@ cd $(dirname -- "$0")
readonly BazelBin="${BAZEL:-bazel} ${BAZEL_STARTUP_FLAGS}"
STALENESS_TESTS=(
"csharp:generated_csharp_defaults_staleness_test"
"java/core:generated_java_defaults_staleness_test"
"upb/reflection:bootstrap_upb_defaults_staleness_test"
"cmake:test_dependencies_staleness"

Loading…
Cancel
Save