@@ -7,6 +7,7 @@ describe('topic_property_extractor.py', () => {
77 const mockSourcePath = path . resolve ( __dirname , 'mock-redpanda-src' ) ;
88 const outputJson = path . resolve ( __dirname , 'topic-properties-output.json' ) ;
99 const outputAdoc = path . resolve ( __dirname , 'topic-properties.adoc' ) ;
10+ const clusterPropsJson = path . resolve ( __dirname , 'mock-cluster-properties.json' ) ;
1011
1112 beforeAll ( ( ) => {
1213 // Create a minimal mock Redpanda source tree
@@ -20,6 +21,7 @@ describe('topic_property_extractor.py', () => {
2021 `inline constexpr std::string_view topic_property_retention_ms = "retention.ms";
2122inline constexpr std::string_view topic_property_segment_bytes = "segment.bytes";
2223inline constexpr std::string_view topic_property_flush_messages = "flush.messages";
24+ inline constexpr std::string_view topic_property_no_mapping = "redpanda.no.mapping";
2325
2426// Mock allowlist for no-op properties
2527inline constexpr std::array<std::string_view, 3> allowlist_topic_noop_confs = {
@@ -34,33 +36,78 @@ inline constexpr std::array<std::string_view, 3> allowlist_topic_noop_confs = {
3436 path . join ( headerDir , 'types.cc' ) ,
3537 `// Copyright 2025 Redpanda Data, Inc.\n#include "kafka/server/handlers/topics/types.h"\n// ...rest of the file...\n`
3638 ) ;
37- // Add a mock config file to simulate a cluster property mapping
38- const configDir = path . join ( mockSourcePath , 'src/v/config ' ) ;
39+ // Add a mock config_response_utils.cc file to simulate cluster property mappings
40+ const configDir = path . join ( mockSourcePath , 'src/v/kafka/server/handlers/configs ' ) ;
3941 fs . mkdirSync ( configDir , { recursive : true } ) ;
4042 fs . writeFileSync (
41- path . join ( configDir , 'mock_config.cc' ) ,
42- 'config.get("log_retention_ms");\nconfig.get("log_segment_size");\n'
43+ path . join ( configDir , 'config_response_utils.cc' ) ,
44+ `// Mock config response utils
45+ add_topic_config_if_requested(
46+ topic_property_retention_ms,
47+ config::shard_local_cfg().log_retention_ms.name(),
48+ config::shard_local_cfg().log_retention_ms.desc()
49+ );
50+
51+ add_topic_config_if_requested(
52+ topic_property_segment_bytes,
53+ config::shard_local_cfg().log_segment_size.name(),
54+ config::shard_local_cfg().log_segment_size.desc()
55+ );
56+
57+ add_topic_config_if_requested(
58+ topic_property_flush_messages,
59+ config::shard_local_cfg().flush_messages.name(),
60+ config::shard_local_cfg().flush_messages.desc()
61+ );
62+ `
4363 ) ;
64+
65+ // Create mock cluster properties JSON with default values
66+ const mockClusterProps = {
67+ properties : {
68+ log_retention_ms : {
69+ name : 'log_retention_ms' ,
70+ type : 'integer' ,
71+ default : 604800000 ,
72+ default_human_readable : '7 days' ,
73+ description : 'Retention time in milliseconds'
74+ } ,
75+ log_segment_size : {
76+ name : 'log_segment_size' ,
77+ type : 'integer' ,
78+ default : 1073741824 ,
79+ description : 'Segment size in bytes'
80+ } ,
81+ flush_messages : {
82+ name : 'flush_messages' ,
83+ type : 'integer' ,
84+ default : 100000 ,
85+ description : 'Number of messages before flush'
86+ }
87+ }
88+ } ;
89+ fs . writeFileSync ( clusterPropsJson , JSON . stringify ( mockClusterProps , null , 2 ) ) ;
4490 }
4591 } ) ;
4692
4793 afterAll ( ( ) => {
4894 // Cleanup
4995 if ( fs . existsSync ( outputJson ) ) fs . unlinkSync ( outputJson ) ;
5096 if ( fs . existsSync ( outputAdoc ) ) fs . unlinkSync ( outputAdoc ) ;
97+ if ( fs . existsSync ( clusterPropsJson ) ) fs . unlinkSync ( clusterPropsJson ) ;
5198 fs . rmdirSync ( mockSourcePath , { recursive : true } ) ;
5299 } ) ;
53100
54101 it ( 'extracts topic properties and generates JSON' , ( ) => {
55- execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-json ${ outputJson } ` ) ;
102+ execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-json ${ outputJson } --cluster-properties-json ${ clusterPropsJson } ` ) ;
56103 const result = JSON . parse ( fs . readFileSync ( outputJson , 'utf8' ) ) ;
57104 expect ( result . topic_properties ) . toBeDefined ( ) ;
58105 expect ( result . topic_properties [ 'retention.ms' ] ) . toBeDefined ( ) ;
59106 expect ( result . topic_properties [ 'retention.ms' ] . property_name ) . toBe ( 'retention.ms' ) ;
60107 } ) ;
61108
62109 it ( 'detects no-op properties correctly' , ( ) => {
63- execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-json ${ outputJson } ` ) ;
110+ execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-json ${ outputJson } --cluster-properties-json ${ clusterPropsJson } ` ) ;
64111 const result = JSON . parse ( fs . readFileSync ( outputJson , 'utf8' ) ) ;
65112
66113 // Check that noop_properties array is present
@@ -81,17 +128,71 @@ inline constexpr std::array<std::string_view, 3> allowlist_topic_noop_confs = {
81128 } ) ;
82129
83130 it ( 'excludes no-op properties from AsciiDoc generation' , ( ) => {
84- execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-adoc ${ outputAdoc } ` ) ;
131+ execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-adoc ${ outputAdoc } --cluster-properties-json ${ clusterPropsJson } ` ) ;
85132 const adoc = fs . readFileSync ( outputAdoc , 'utf8' ) ;
86-
87- // Should contain regular properties
133+
134+ // Should contain regular properties in the documentation
88135 expect ( adoc ) . toContain ( '= Topic Configuration Properties' ) ;
89136 expect ( adoc ) . toContain ( 'retention.ms' ) ;
90137 expect ( adoc ) . toContain ( 'segment.bytes' ) ;
91-
138+
92139 // Should NOT contain no-op properties in documentation
93140 expect ( adoc ) . not . toContain ( 'flush.messages' ) ;
94141 expect ( adoc ) . not . toContain ( 'segment.index.bytes' ) ;
95142 expect ( adoc ) . not . toContain ( 'preallocate' ) ;
143+
144+ // Properties with cluster mappings should appear in the mappings table
145+ expect ( adoc ) . toMatch ( / T o p i c p r o p e r t y m a p p i n g s [ \s \S ] * r e t e n t i o n \. m s [ \s \S ] * l o g _ r e t e n t i o n _ m s / ) ;
146+ expect ( adoc ) . toMatch ( / T o p i c p r o p e r t y m a p p i n g s [ \s \S ] * s e g m e n t \. b y t e s [ \s \S ] * l o g _ s e g m e n t _ s i z e / ) ;
147+ } ) ;
148+
149+ it ( 'documents properties without cluster mappings' , ( ) => {
150+ execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-adoc ${ outputAdoc } --cluster-properties-json ${ clusterPropsJson } ` ) ;
151+ const adoc = fs . readFileSync ( outputAdoc , 'utf8' ) ;
152+
153+ // Property without cluster mapping should appear in the main documentation
154+ expect ( adoc ) . toContain ( 'redpanda.no.mapping' ) ;
155+
156+ // Extract the mappings table section (between "Topic property mappings" and "== Topic properties")
157+ const mappingsSection = adoc . match ( / T o p i c p r o p e r t y m a p p i n g s [ \s \S ] * ?(? = = = \s + T o p i c p r o p e r t i e s ) / ) ;
158+
159+ // Property without mapping should NOT appear in the mappings table
160+ if ( mappingsSection ) {
161+ expect ( mappingsSection [ 0 ] ) . not . toContain ( 'redpanda.no.mapping' ) ;
162+ }
163+
164+ // But it should appear in the Topic properties section
165+ const propertiesSection = adoc . match ( / = = \s + T o p i c p r o p e r t i e s [ \s \S ] * / ) ;
166+ expect ( propertiesSection [ 0 ] ) . toContain ( 'redpanda.no.mapping' ) ;
167+
168+ // And should not have a "Related cluster property" line
169+ const noMappingSection = adoc . match ( / = = = \s + r e d p a n d a \. n o \. m a p p i n g [ \s \S ] * ?- - - / ) ;
170+ expect ( noMappingSection ) . toBeTruthy ( ) ;
171+ expect ( noMappingSection [ 0 ] ) . not . toContain ( 'Related cluster property' ) ;
172+ } ) ;
173+
174+ it ( 'populates default values from cluster properties' , ( ) => {
175+ execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-json ${ outputJson } --cluster-properties-json ${ clusterPropsJson } ` ) ;
176+ const result = JSON . parse ( fs . readFileSync ( outputJson , 'utf8' ) ) ;
177+
178+ // Check that default values are populated from cluster properties
179+ expect ( result . topic_properties [ 'retention.ms' ] . default ) . toBe ( 604800000 ) ;
180+ expect ( result . topic_properties [ 'retention.ms' ] . default_human_readable ) . toBe ( '7 days' ) ;
181+ expect ( result . topic_properties [ 'segment.bytes' ] . default ) . toBe ( 1073741824 ) ;
182+
183+ // Property without cluster mapping should not have a default
184+ expect ( result . topic_properties [ 'redpanda.no.mapping' ] . default ) . toBeNull ( ) ;
185+ } ) ;
186+
187+ it ( 'populates default values in JSON for use by Handlebars templates' , ( ) => {
188+ // The Python script doesn't format defaults in AsciiDoc - that's handled by Handlebars
189+ // But it should populate the default field in the JSON output
190+ execSync ( `python3 ${ scriptPath } --source-path ${ mockSourcePath } --output-json ${ outputJson } --cluster-properties-json ${ clusterPropsJson } ` ) ;
191+ const result = JSON . parse ( fs . readFileSync ( outputJson , 'utf8' ) ) ;
192+
193+ // Verify the JSON has raw default values that Handlebars will format
194+ expect ( result . topic_properties [ 'retention.ms' ] . default ) . toBe ( 604800000 ) ;
195+ expect ( result . topic_properties [ 'retention.ms' ] . default_human_readable ) . toBe ( '7 days' ) ;
196+ expect ( result . topic_properties [ 'segment.bytes' ] . default ) . toBe ( 1073741824 ) ;
96197 } ) ;
97198} ) ;
0 commit comments