Skip to content

Commit

Permalink
Update C# FeatureSetDescriptor to use auto-generated source of truth.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 692879910
  • Loading branch information
protobuf-github-bot authored and copybara-github committed Nov 4, 2024
1 parent 2fe8aaa commit 4d72a22
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 58 deletions.
33 changes: 33 additions & 0 deletions csharp/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"],
)
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
83 changes: 44 additions & 39 deletions csharp/src/Google.Protobuf/Reflection/FeatureSetDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
1 change: 1 addition & 0 deletions regenerate_stale_files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 4d72a22

Please sign in to comment.