Skip to content

Commit 2d0a89b

Browse files
authored
Merge pull request #1391 from godot-rust/bugfix/register-doc-ci
Fix non-deterministic `register-docs` XML output
2 parents 69c346e + 7e92794 commit 2d0a89b

File tree

7 files changed

+144
-96
lines changed

7 files changed

+144
-96
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ exp.rs
2929

3030
# Windows specific
3131
desktop.ini
32+
33+
# Test outputs
34+
/itest/rust/src/register_tests/res/actual_registered_docs.xml

godot-core/src/docs.rs

Lines changed: 67 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub struct StructDocs {
6060
pub description: &'static str,
6161
pub experimental: &'static str,
6262
pub deprecated: &'static str,
63-
pub members: &'static str,
63+
pub properties: &'static str,
6464
}
6565

6666
/// Keeps documentation for inherent `impl` blocks (primary and secondary), such as:
@@ -82,17 +82,20 @@ pub struct StructDocs {
8282
/// All fields are XML parts, escaped where necessary.
8383
#[derive(Default, Clone, Debug)]
8484
pub struct InherentImplDocs {
85-
pub methods: &'static str,
86-
pub signals: &'static str,
87-
pub constants: &'static str,
85+
pub methods_xml: &'static str,
86+
pub signals_xml: &'static str,
87+
pub constants_xml: &'static str,
8888
}
8989

90+
/// Godot editor documentation for a class, combined from individual definitions (struct + impls).
91+
///
92+
/// All fields are collections of XML parts, escaped where necessary.
9093
#[derive(Default)]
91-
struct DocPieces {
94+
struct AggregatedDocs {
9295
definition: StructDocs,
93-
methods: Vec<&'static str>,
94-
signals: Vec<&'static str>,
95-
constants: Vec<&'static str>,
96+
methods_xmls: Vec<&'static str>,
97+
signals_xmls: Vec<&'static str>,
98+
constants_xmls: Vec<&'static str>,
9699
}
97100

98101
/// This function scours the registered plugins to find their documentation pieces,
@@ -110,77 +113,79 @@ struct DocPieces {
110113
/// strings of not-yet-parented XML tags (or empty string if no method has been documented).
111114
#[doc(hidden)]
112115
pub fn gather_xml_docs() -> impl Iterator<Item = String> {
113-
let mut map = HashMap::<ClassId, DocPieces>::new();
114-
crate::private::iterate_docs_plugins(|x| {
115-
let class_name = x.class_name;
116-
match &x.item {
117-
DocsItem::Struct(s) => {
118-
map.entry(class_name).or_default().definition = *s;
116+
let mut map = HashMap::<ClassId, AggregatedDocs>::new();
117+
118+
crate::private::iterate_docs_plugins(|shard| {
119+
let class_name = shard.class_name;
120+
match &shard.item {
121+
DocsItem::Struct(struct_docs) => {
122+
map.entry(class_name).or_default().definition = *struct_docs;
119123
}
124+
120125
DocsItem::InherentImpl(trait_docs) => {
121-
let InherentImplDocs {
122-
methods,
123-
constants,
124-
signals,
125-
} = trait_docs;
126-
map.entry(class_name).or_default().methods.push(methods);
127126
map.entry(class_name)
128-
.and_modify(|pieces| pieces.constants.push(constants));
127+
.or_default()
128+
.methods_xmls
129+
.push(trait_docs.methods_xml);
130+
129131
map.entry(class_name)
130-
.and_modify(|pieces| pieces.signals.push(signals));
132+
.and_modify(|pieces| pieces.constants_xmls.push(trait_docs.constants_xml));
133+
134+
map.entry(class_name)
135+
.and_modify(|pieces| pieces.signals_xmls.push(trait_docs.signals_xml));
131136
}
132-
DocsItem::ITraitImpl(methods) => {
133-
map.entry(class_name).or_default().methods.push(methods);
137+
138+
DocsItem::ITraitImpl(methods_xml) => {
139+
map.entry(class_name)
140+
.or_default()
141+
.methods_xmls
142+
.push(methods_xml);
134143
}
135144
}
136145
});
137146

138147
map.into_iter().map(|(class, pieces)| {
139-
let StructDocs {
140-
base,
141-
description,
142-
experimental,
143-
deprecated,
144-
members,
145-
} = pieces.definition;
146-
147-
148-
let method_docs = String::from_iter(pieces.methods);
149-
let signal_docs = String::from_iter(pieces.signals);
150-
let constant_docs = String::from_iter(pieces.constants);
151-
152-
let methods_block = if method_docs.is_empty() {
153-
String::new()
154-
} else {
155-
format!("<methods>{method_docs}</methods>")
156-
};
157-
let signals_block = if signal_docs.is_empty() {
158-
String::new()
159-
} else {
160-
format!("<signals>{signal_docs}</signals>")
161-
};
162-
let constants_block = if constant_docs.is_empty() {
163-
String::new()
164-
} else {
165-
format!("<constants>{constant_docs}</constants>")
166-
};
167-
let (brief, description) = match description
168-
.split_once("[br]") {
169-
Some((brief, description)) => (brief, description.trim_start_matches("[br]")),
170-
None => (description, ""),
171-
};
172-
173-
format!(r#"<?xml version="1.0" encoding="UTF-8"?>
148+
let StructDocs {
149+
base,
150+
description,
151+
experimental,
152+
deprecated,
153+
properties,
154+
} = pieces.definition;
155+
156+
let methods_block = wrap_in_xml_block("methods", pieces.methods_xmls);
157+
let signals_block = wrap_in_xml_block("signals", pieces.signals_xmls);
158+
let constants_block = wrap_in_xml_block("constants", pieces.constants_xmls);
159+
160+
let (brief, description) = match description.split_once("[br]") {
161+
Some((brief, description)) => (brief, description.trim_start_matches("[br]")),
162+
None => (description, ""),
163+
};
164+
165+
format!(r#"<?xml version="1.0" encoding="UTF-8"?>
174166
<class name="{class}" inherits="{base}"{deprecated}{experimental} xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
175167
<brief_description>{brief}</brief_description>
176168
<description>{description}</description>
177169
{methods_block}
178170
{constants_block}
179171
{signals_block}
180-
<members>{members}</members>
172+
<members>{properties}</members>
181173
</class>"#)
182-
},
183-
)
174+
})
175+
}
176+
177+
fn wrap_in_xml_block(tag: &str, mut blocks: Vec<&'static str>) -> String {
178+
// We sort the blocks for deterministic output. No need to sort individual methods/signals/constants, this is already done by Godot.
179+
// See https://github.com/godot-rust/gdext/pull/1391 for more information.
180+
blocks.sort();
181+
182+
let content = String::from_iter(blocks);
183+
184+
if content.is_empty() {
185+
String::new()
186+
} else {
187+
format!("<{tag}>{content}</{tag}>")
188+
}
184189
}
185190

186191
/// # Safety

godot-macros/src/docs/extract_docs.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,28 @@ pub fn document_struct(
3434
description: &[venial::Attribute],
3535
fields: &[Field],
3636
) -> TokenStream {
37-
let base_escaped = xml_escape(base);
3837
let XmlParagraphs {
3938
description_content,
4039
deprecated_attr,
4140
experimental_attr,
4241
} = attribute_docs_to_xml_paragraphs(description).unwrap_or_default();
4342

44-
let members = fields
43+
let properties = fields
4544
.iter()
4645
.filter(|field| field.var.is_some() || field.export.is_some())
4746
.filter_map(format_member_xml)
4847
.collect::<String>();
4948

49+
let base_escaped = xml_escape(base);
50+
5051
quote! {
51-
::godot::docs::StructDocs {
52-
base: #base_escaped,
53-
description: #description_content,
54-
experimental: #experimental_attr,
55-
deprecated: #deprecated_attr,
56-
members: #members,
57-
}
52+
::godot::docs::StructDocs {
53+
base: #base_escaped,
54+
description: #description_content,
55+
experimental: #experimental_attr,
56+
deprecated: #deprecated_attr,
57+
properties: #properties,
58+
}
5859
}
5960
}
6061

godot-macros/src/docs/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ mod docs_generators {
5151

5252
quote! {
5353
::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>(
54-
#prv::DocsItem::InherentImpl(#prv::InherentImplDocs {
55-
methods: #method_xml_elems,
56-
signals: #signal_xml_elems,
57-
constants: #constant_xml_elems
58-
})
54+
#prv::DocsItem::InherentImpl(#prv::InherentImplDocs {
55+
methods_xml: #method_xml_elems,
56+
signals_xml: #signal_xml_elems,
57+
constants_xml: #constant_xml_elems
58+
})
5959
));
6060
}
6161
}
@@ -69,7 +69,7 @@ mod docs_generators {
6969

7070
quote! {
7171
::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>(
72-
#prv::DocsItem::ITraitImpl(#virtual_methods)
72+
#prv::DocsItem::ITraitImpl(#virtual_methods)
7373
));
7474
}
7575
}

itest/rust/src/framework/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,14 @@ pub fn runs_release() -> bool {
321321
!Os::singleton().is_debug_build()
322322
}
323323

324+
/// Whether we are running in GitHub Actions CI.
325+
///
326+
/// Must not be used to influence test logic. Only for logging and diagnostics.
327+
#[allow(dead_code)]
328+
pub fn runs_github_ci() -> bool {
329+
std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true")
330+
}
331+
324332
/// Create a `GDScript` script from code, compiles and returns it.
325333
pub fn create_gdscript(code: &str) -> Gd<GDScript> {
326334
let mut script = GDScript::new_gd();

itest/rust/src/register_tests/register_docs_test.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,18 +316,49 @@ impl FairlyDocumented {
316316
impl FairlyDocumented {
317317
/// Documented method in other godot_api secondary block
318318
#[func]
319-
fn trinary_but_documented(&self, _smth: i64) {}
319+
fn tertiary_but_documented(&self, _smth: i64) {}
320320
}
321321

322322
#[itest]
323323
fn test_register_docs() {
324-
let xml = find_class_docs("FairlyDocumented");
324+
let actual_xml = find_class_docs("FairlyDocumented");
325325

326326
// Uncomment if implementation changes and expected output file should be rewritten.
327327
// std::fs::write("../rust/src/register_tests/res/registered_docs.xml", &xml)
328328
// .expect("failed to write docs XML file");
329329

330-
assert_eq!(include_str!("res/registered_docs.xml"), xml);
330+
let expected_xml = include_str!("res/registered_docs.xml");
331+
332+
if actual_xml == expected_xml {
333+
return; // All good.
334+
}
335+
336+
// In GitHub Actions, print output of expected vs. actual.
337+
if crate::framework::runs_github_ci() {
338+
panic!(
339+
"Registered docs XML does not match expected output.\
340+
\n============================================================\
341+
\nExpected:\n\n{expected_xml}\n\
342+
\n------------------------------------------------------------\
343+
\nActual:\n\n{actual_xml}\n\
344+
\n============================================================\n"
345+
);
346+
}
347+
348+
// Locally, write to a file for manual inspection/diffing.
349+
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
350+
.join("src")
351+
.join("register_tests")
352+
.join("res")
353+
.join("actual_registered_docs.xml");
354+
355+
std::fs::write(&path, &actual_xml).expect("write `actual_registered_docs.xml` failed");
356+
357+
panic!(
358+
"Registered docs XML does not match expected output.\n\
359+
Actual output has been written to following file:\n {path}\n",
360+
path = path.display()
361+
);
331362
}
332363

333364
fn find_class_docs(class_name: &str) -> String {

itest/rust/src/register_tests/res/registered_docs.xml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ public class Player : Node2D
2525
</description>
2626
</method>
2727

28+
<method name="secondary_but_documented">
29+
<return type="()" />
30+
<param index="0" name="smth" type="i64" />
31+
<description>
32+
Documented method in godot_api secondary block
33+
</description>
34+
</method>
35+
36+
<method name="tertiary_but_documented">
37+
<return type="()" />
38+
<param index="0" name="smth" type="i64" />
39+
<description>
40+
Documented method in other godot_api secondary block
41+
</description>
42+
</method>
43+
2844
<method name="ye">
2945
<return type="f32" />
3046

@@ -72,22 +88,6 @@ public class Player : Node2D
7288
????? probably
7389
</description>
7490
</method>
75-
76-
<method name="secondary_but_documented">
77-
<return type="()" />
78-
<param index="0" name="smth" type="i64" />
79-
<description>
80-
Documented method in godot_api secondary block
81-
</description>
82-
</method>
83-
84-
<method name="trinary_but_documented">
85-
<return type="()" />
86-
<param index="0" name="smth" type="i64" />
87-
<description>
88-
Documented method in other godot_api secondary block
89-
</description>
90-
</method>
9191
</methods>
9292
<constants><constant name="RANDOM" value="4">Documentation.</constant><constant name="A" value="128" deprecated="Did you know that constants can be deprecated?">Hmmmm</constant><constant name="B" value="128" experimental="Did you know that constants can be experimental?">Who would know that!</constant><constant name="XML" value="1">this docstring has &lt; a special character</constant></constants>
9393
<signals>

0 commit comments

Comments
 (0)