Skip to content

Commit 4d72a22

Browse files
Update C# FeatureSetDescriptor to use auto-generated source of truth.
PiperOrigin-RevId: 692879910
1 parent 2fe8aaa commit 4d72a22

File tree

5 files changed

+140
-58
lines changed

5 files changed

+140
-58
lines changed

csharp/BUILD.bazel

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix")
66
load("//build_defs:internal_shell.bzl", "inline_sh_test")
77
load("//conformance:defs.bzl", "conformance_test")
8+
load("//editions:defaults.bzl", "compile_edition_defaults", "embed_edition_defaults")
9+
load("//upb/cmake:build_defs.bzl", "staleness_test")
810

911
################################################################################
1012
# Tests
@@ -114,3 +116,34 @@ sh_binary(
114116
srcs = ["build_release.sh"],
115117
args = ["$(location build_release.sh)"],
116118
)
119+
120+
################################################################################
121+
# Generated edition defaults (and staleness test)
122+
################################################################################
123+
124+
compile_edition_defaults(
125+
name = "csharp_edition_defaults",
126+
srcs = [
127+
"//:descriptor_proto",
128+
],
129+
maximum_edition = "2023",
130+
minimum_edition = "PROTO2",
131+
)
132+
133+
# TODO Make bazel tests use this output instead of the checked-in one
134+
embed_edition_defaults(
135+
name = "embedded_csharp_edition_defaults_generate",
136+
defaults = "csharp_edition_defaults",
137+
encoding = "base64",
138+
output = "generated/src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs",
139+
placeholder = "DEFAULTS_VALUE",
140+
template = "src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs.template",
141+
)
142+
143+
staleness_test(
144+
name = "generated_csharp_defaults_staleness_test",
145+
outs = ["src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs"],
146+
generated_pattern = "generated/%s",
147+
tags = ["manual"],
148+
target_files = ["src/Google.Protobuf/Reflection/FeatureSetDescriptor.g.cs"],
149+
)

csharp/src/Google.Protobuf.Test/Reflection/FeatureSetDescriptorTest.cs

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,59 @@
1010
using Google.Protobuf.Reflection;
1111
using NUnit.Framework;
1212
using System;
13-
using System.Linq;
13+
using static Google.Protobuf.Reflection.FeatureSet.Types;
1414

1515
namespace Google.Protobuf.Test.Reflection;
1616

1717
public class FeatureSetDescriptorTest
1818
{
19-
// Canonical serialized form of the edition defaults, generated by embed_edition_defaults.
20-
// TODO: Update this automatically.
21-
private const string DefaultsBase64 =
22-
"ChMY5gciDAgBEAIYAiADKAEwAioAChMY5wciDAgCEAEYASACKAEwASoAChMY6AciDAgBEAEYASACKAEwASoAIOYHKOgH";
19+
// Just selectively test a couple of hard-coded examples. This isn't meant to be exhaustive,
20+
// and we don't expect to add new tests for later editions unless there's a production code change.
2321

2422
[Test]
25-
[TestCase(Edition.Proto2)]
26-
[TestCase(Edition.Proto3)]
27-
[TestCase(Edition._2023)]
28-
public void DefaultsMatchCanonicalSerializedForm(Edition edition)
23+
public void Proto2Defaults()
2924
{
30-
var canonicalDefaults = FeatureSetDefaults.Parser
31-
.WithDiscardUnknownFields(true) // Discard language-specific extensions.
32-
.ParseFrom(Convert.FromBase64String(DefaultsBase64));
33-
var canonicalEditionDefaults = new FeatureSet();
34-
canonicalEditionDefaults.MergeFrom(
35-
canonicalDefaults.Defaults.Single(def => def.Edition == edition).FixedFeatures);
36-
canonicalEditionDefaults.MergeFrom(
37-
canonicalDefaults.Defaults.Single(def => def.Edition == edition).OverridableFeatures);
38-
var candidateEditionDefaults = FeatureSetDescriptor.GetEditionDefaults(edition).Proto;
25+
var expectedDefaults = new FeatureSet
26+
{
27+
EnumType = EnumType.Closed,
28+
FieldPresence = FieldPresence.Explicit,
29+
JsonFormat = JsonFormat.LegacyBestEffort,
30+
MessageEncoding = MessageEncoding.LengthPrefixed,
31+
RepeatedFieldEncoding = RepeatedFieldEncoding.Expanded,
32+
Utf8Validation = Utf8Validation.None,
33+
};
34+
var actualDefaults = FeatureSetDescriptor.GetEditionDefaults(Edition.Proto2).Proto;
35+
Assert.AreEqual(expectedDefaults, actualDefaults);
36+
}
37+
38+
[Test]
39+
public void Proto3Defaults()
40+
{
41+
var expectedDefaults = new FeatureSet
42+
{
43+
EnumType = EnumType.Open,
44+
FieldPresence = FieldPresence.Implicit,
45+
JsonFormat = JsonFormat.Allow,
46+
MessageEncoding = MessageEncoding.LengthPrefixed,
47+
RepeatedFieldEncoding = RepeatedFieldEncoding.Packed,
48+
Utf8Validation = Utf8Validation.Verify,
49+
};
50+
var actualDefaults = FeatureSetDescriptor.GetEditionDefaults(Edition.Proto3).Proto;
51+
Assert.AreEqual(expectedDefaults, actualDefaults);
52+
}
53+
54+
[Test]
55+
public void MaxSupportedEdition()
56+
{
57+
// This should be the last piece of code to be changed when updating the C# runtime to support
58+
// a new edition. It should only be changed when you're sure that all the features in the new
59+
// edition are supported. Just changing the configuration for feature set default generation
60+
// will *advertise* that we support the new edition, but that isn't sufficient.
61+
Edition maxSupportedEdition = Edition._2023;
3962

40-
Assert.AreEqual(canonicalEditionDefaults, candidateEditionDefaults);
63+
// These lines should not need to be changed.
64+
FeatureSetDescriptor.GetEditionDefaults(maxSupportedEdition);
65+
Edition invalidEdition = (Edition) (maxSupportedEdition + 1);
66+
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureSetDescriptor.GetEditionDefaults(invalidEdition));
4167
}
4268
}

csharp/src/Google.Protobuf/Reflection/FeatureSetDescriptor.cs

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
using System;
1111
using System.Collections.Concurrent;
12+
using System.Collections.Generic;
13+
using System.Linq;
1214
using static Google.Protobuf.Reflection.FeatureSet.Types;
1315

1416
namespace Google.Protobuf.Reflection;
@@ -22,52 +24,55 @@ namespace Google.Protobuf.Reflection;
2224
/// If either of those features are ever implemented in this runtime,
2325
/// the feature settings will be exposed as properties in this class.
2426
/// </remarks>
25-
internal sealed class FeatureSetDescriptor
27+
internal sealed partial class FeatureSetDescriptor
2628
{
2729
private static readonly ConcurrentDictionary<FeatureSet, FeatureSetDescriptor> cache = new();
2830

29-
// Note: this approach is deliberately chosen to circumvent bootstrapping issues.
30-
// This can still be tested using the binary representation.
31-
// TODO: Generate this code (as a partial class) from the binary representation.
32-
private static readonly FeatureSetDescriptor edition2023Defaults = new FeatureSetDescriptor(
33-
new FeatureSet
34-
{
35-
EnumType = EnumType.Open,
36-
FieldPresence = FieldPresence.Explicit,
37-
JsonFormat = JsonFormat.Allow,
38-
MessageEncoding = MessageEncoding.LengthPrefixed,
39-
RepeatedFieldEncoding = RepeatedFieldEncoding.Packed,
40-
Utf8Validation = Utf8Validation.Verify,
41-
});
42-
private static readonly FeatureSetDescriptor proto2Defaults = new FeatureSetDescriptor(
43-
new FeatureSet
31+
private static readonly IReadOnlyDictionary<Edition, FeatureSetDescriptor> descriptorsByEdition = BuildEditionDefaults();
32+
33+
// Note: if the debugger is set to break within this code, various type initializers will fail
34+
// as the debugger will try to call ToString() on messages, requiring descriptors to be accessed etc.
35+
// There's a possible workaround of using a hard-coded bootstrapping FeatureSetDescriptor to be returned
36+
// by GetEditionDefaults if descriptorsByEdition is null, but it's ugly and likely just pushes the problem
37+
// elsewhere. Normal debugging sessions (where the initial bootstrapping code doesn't hit any breakpoints)
38+
// do not cause any problems.
39+
private static IReadOnlyDictionary<Edition, FeatureSetDescriptor> BuildEditionDefaults()
40+
{
41+
var featureSetDefaults = FeatureSetDefaults.Parser.ParseFrom(Convert.FromBase64String(DefaultsBase64));
42+
var ret = new Dictionary<Edition, FeatureSetDescriptor>();
43+
44+
// Note: Enum.GetValues<TEnum> isn't available until .NET 5. It's not worth making this conditional
45+
// based on that.
46+
var supportedEditions = ((Edition[]) Enum.GetValues(typeof(Edition)))
47+
.OrderBy(x => x)
48+
.Where(e => e >= featureSetDefaults.MinimumEdition && e <= featureSetDefaults.MaximumEdition);
49+
50+
// We assume the embedded defaults will always contain "legacy".
51+
var currentDescriptor = MaybeCreateDescriptor(Edition.Legacy);
52+
foreach (var edition in supportedEditions)
4453
{
45-
EnumType = EnumType.Closed,
46-
FieldPresence = FieldPresence.Explicit,
47-
JsonFormat = JsonFormat.LegacyBestEffort,
48-
MessageEncoding = MessageEncoding.LengthPrefixed,
49-
RepeatedFieldEncoding = RepeatedFieldEncoding.Expanded,
50-
Utf8Validation = Utf8Validation.None,
51-
});
52-
private static readonly FeatureSetDescriptor proto3Defaults = new FeatureSetDescriptor(
53-
new FeatureSet
54+
currentDescriptor = MaybeCreateDescriptor(edition) ?? currentDescriptor;
55+
ret[edition] = currentDescriptor;
56+
}
57+
return ret;
58+
59+
FeatureSetDescriptor MaybeCreateDescriptor(Edition edition)
5460
{
55-
EnumType = EnumType.Open,
56-
FieldPresence = FieldPresence.Implicit,
57-
JsonFormat = JsonFormat.Allow,
58-
MessageEncoding = MessageEncoding.LengthPrefixed,
59-
RepeatedFieldEncoding = RepeatedFieldEncoding.Packed,
60-
Utf8Validation = Utf8Validation.Verify,
61-
});
61+
var editionDefaults = featureSetDefaults.Defaults.SingleOrDefault(d => d.Edition == edition);
62+
if (editionDefaults is null)
63+
{
64+
return null;
65+
}
66+
var proto = new FeatureSet();
67+
proto.MergeFrom(editionDefaults.FixedFeatures);
68+
proto.MergeFrom(editionDefaults.OverridableFeatures);
69+
return new FeatureSetDescriptor(proto);
70+
}
71+
}
6272

6373
internal static FeatureSetDescriptor GetEditionDefaults(Edition edition) =>
64-
edition switch
65-
{
66-
Edition.Proto2 => proto2Defaults,
67-
Edition.Proto3 => proto3Defaults,
68-
Edition._2023 => edition2023Defaults,
69-
_ => throw new ArgumentOutOfRangeException($"Unsupported edition: {edition}")
70-
};
74+
descriptorsByEdition.TryGetValue(edition, out var defaults) ? defaults
75+
: throw new ArgumentOutOfRangeException($"Unsupported edition: {edition}");
7176

7277
// Visible for testing. The underlying feature set proto, usually derived during
7378
// feature resolution.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#region Copyright notice and license
2+
// Protocol Buffers - Google's data interchange format
3+
// Copyright 2008 Google Inc. All rights reserved.
4+
//
5+
// Use of this source code is governed by a BSD-style
6+
// license that can be found in the LICENSE file or at
7+
// https://developers.google.com/open-source/licenses/bsd
8+
#endregion
9+
10+
namespace Google.Protobuf.Reflection;
11+
12+
internal sealed partial class FeatureSetDescriptor
13+
{
14+
// Canonical serialized form of the edition defaults, generated by embed_edition_defaults.
15+
private const string DefaultsBase64 =
16+
"DEFAULTS_VALUE";
17+
}

regenerate_stale_files.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ cd $(dirname -- "$0")
1313
readonly BazelBin="${BAZEL:-bazel} ${BAZEL_STARTUP_FLAGS}"
1414

1515
STALENESS_TESTS=(
16+
"csharp:generated_csharp_defaults_staleness_test"
1617
"java/core:generated_java_defaults_staleness_test"
1718
"upb/reflection:bootstrap_upb_defaults_staleness_test"
1819
"cmake:test_dependencies_staleness"

0 commit comments

Comments
 (0)