Skip to content

Commit 697cf62

Browse files
authored
Merge pull request #1003 from ThrowTheSwitch/feature/mixin_smarter_merges
Smarter Mixin merges + C23 language compatibility fixes & workarounds
2 parents 3cd45ef + 89928fc commit 697cf62

File tree

13 files changed

+827
-61
lines changed

13 files changed

+827
-61
lines changed

assets/test_example_file_crash.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ void test_add_numbers_adds_numbers(void) {
1919

2020
void test_add_numbers_will_fail(void) {
2121
// Platform-independent way of forcing a crash
22-
uint32_t* nullptr = (void*) 0;
23-
uint32_t i = *nullptr;
22+
// NOTE: Avoid `nullptr` as it is a keyword in C23
23+
uint32_t* null_ptr = (void*) 0;
24+
uint32_t i = *null_ptr;
2425
TEST_ASSERT_EQUAL_INT(2, add_numbers(i,2));
2526
}

bin/ceedling

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ begin
7171
# This prevents double instantiation and preserves object state in handoff
7272
handoff_objects = {}
7373
handoff = [
74+
:reportinator,
7475
:loginator,
7576
:file_wrapper,
7677
:yaml_wrapper,

bin/configinator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def loadinate(builtin_mixins:, filepath:nil, mixins:[], env:{}, silent:false)
8888
)
8989

9090
# Merge mixins
91-
@mixinator.merge( builtins:builtin_mixins, config:config, mixins:mixins_assembled )
91+
@mixinator.mixin( builtins:builtin_mixins, config:config, mixins:mixins_assembled )
9292

9393
return project_filepath, config
9494
end

bin/merginator.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# =========================================================================
2+
# Ceedling - Test-Centered Build System for C
3+
# ThrowTheSwitch.org
4+
# Copyright (c) 2010-25 Mike Karlesky, Mark VanderVoord, & Greg Williams
5+
# SPDX-License-Identifier: MIT
6+
# =========================================================================
7+
8+
require 'deep_merge'
9+
10+
class Merginator
11+
12+
constructor :reportinator
13+
14+
def setup
15+
# ...
16+
end
17+
18+
19+
def merge(config:, mixin:, warnings:)
20+
# Find any incompatible merge values in config and mixin
21+
validate = validate_merge( config:config, mixin:mixin, mismatches:warnings )
22+
23+
# Merge this bad boy
24+
config.deep_merge!(
25+
mixin,
26+
# In cases of a primitive and a hash of primitives, add single item to array
27+
# This handles merge cases where valid config entries can be a single string or array of strings
28+
# Because of the nature of Ceedling configurations, this handling is primarily for the string case
29+
:extend_existing_arrays => true
30+
)
31+
32+
return validate
33+
end
34+
35+
### Private
36+
37+
private
38+
39+
# Recursive inspection of mergeable base config & mixin
40+
def validate_merge(config:, mixin:, key_path:[], mismatches:)
41+
# Track whether all matching hash key paths have matching value types
42+
valid = true
43+
44+
# Get all keys from both hashes
45+
all_keys = (config.keys + mixin.keys).uniq
46+
47+
all_keys.each do |key|
48+
current_path = key_path + [key]
49+
50+
# Skip if key doesn't exist in both hashes
51+
next unless config.key?(key) && mixin.key?(key)
52+
53+
config_value = config[key]
54+
mixin_value = mixin[key]
55+
56+
if config_value.is_a?(Hash) && mixin_value.is_a?(Hash)
57+
# Recursively check nested hashes
58+
sub_result = validate_merge(
59+
config: config_value,
60+
mixin: mixin_value,
61+
key_path: current_path,
62+
mismatches: mismatches
63+
)
64+
65+
valid = false unless sub_result
66+
67+
# Skip comparing anything where the config value is an array as it will be extended by the mixin value
68+
elsif !config_value.is_a?(Array)
69+
# Compare types of non-hash values
70+
unless config_value.class == mixin_value.class
71+
# If mergeable values at key paths in common are not the same type, register this
72+
valid = false
73+
key_path_str = @reportinator.generate_config_walk( current_path )
74+
warning = "Incompatible merge at key path #{key_path_str} ==> Project configuration has #{config_value.class} while Mixin has #{mixin_value.class}"
75+
76+
# Do not use `<<` as it is locally scoped
77+
mismatches.push( warning )
78+
end
79+
end
80+
end
81+
82+
return valid
83+
end
84+
85+
end

bin/mixin_standardizer.rb

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# =========================================================================
2+
# Ceedling - Test-Centered Build System for C
3+
# ThrowTheSwitch.org
4+
# Copyright (c) 2010-25 Mike Karlesky, Mark VanderVoord, & Greg Williams
5+
# SPDX-License-Identifier: MIT
6+
# =========================================================================
7+
8+
class MixinStandardizer
9+
10+
constructor :reportinator
11+
12+
def setup
13+
# ...
14+
end
15+
16+
def smart_standardize(config:, mixin:, notices:)
17+
modified = false
18+
modified |= smart_standardize_defines(config, mixin, notices)
19+
modified |= smart_standardize_flags(config, mixin, notices)
20+
return modified
21+
end
22+
23+
### Private
24+
25+
private
26+
27+
def smart_standardize_defines(config, mixin, notices)
28+
modified = false
29+
30+
# Bail out if config and mixin do noth both have :defines
31+
return false unless config[:defines] && mixin[:defines]
32+
33+
# Iterate over :defines ↳ <context> keys
34+
# If both config and mixin contain the same key paths, process their (matcher) values
35+
config[:defines].each do |context, context_hash|
36+
if mixin[:defines][context]
37+
38+
# Standardize :defines ↳ <context> matcher conventions if they differ so they can be merged later
39+
standardized, notice = standardize_matchers(
40+
config[:defines][context],
41+
mixin[:defines][context],
42+
config[:defines],
43+
mixin[:defines],
44+
context
45+
)
46+
47+
if standardized
48+
path, _ = @reportinator.generate_config_walk( [:defines, context] )
49+
_notice = "At #{path}: #{notice}"
50+
notices.push( _notice )
51+
end
52+
53+
modified |= standardized
54+
end
55+
end
56+
57+
return modified
58+
end
59+
60+
def smart_standardize_flags(config, mixin, notices)
61+
modified = false
62+
63+
# Bail out if config and mixin do noth both have :flags
64+
return false unless config[:flags] && mixin[:flags]
65+
66+
# Iterate over :flags ↳ <context> ↳ <operation> keys
67+
# If both config and mixin contain the same key paths, process their (matcher) values
68+
config[:flags].each do |context, context_hash|
69+
next unless mixin[:flags][context]
70+
71+
context_hash.each do |operation, operation_hash|
72+
if mixin[:flags][context][operation]
73+
74+
# Standardize :flags ↳ <context> ↳ <operation> matcher conventions if they differ so they can be merged later
75+
standardized, notice = standardize_matchers(
76+
config[:flags][context][operation],
77+
mixin[:flags][context][operation],
78+
config[:flags][context],
79+
mixin[:flags][context],
80+
operation
81+
)
82+
83+
if standardized
84+
path, _ = @reportinator.generate_config_walk( [:flags, context, operation] )
85+
_notice = "At #{path}: #{notice}"
86+
notices.push( _notice )
87+
end
88+
89+
modified |= standardized
90+
end
91+
end
92+
end
93+
94+
return modified
95+
end
96+
97+
def standardize_matchers(config_value, mixin_value, config_parent, mixin_parent, key)
98+
# If both values are the same type, do nothing
99+
return false, nil if (config_value.class == mixin_value.class)
100+
101+
# Promote mixin value list to all-matches matcher hash
102+
if config_value.is_a?(Hash) && mixin_value.is_a?(Array)
103+
# Ensure all-matches matcher key is a symbol and not a string
104+
config_value[:*] = value if value = config_value.delete( '*' )
105+
106+
# Replace the value of a simple array list with a matcher hash that stores the original list
107+
mixin_parent[key] = {:* => mixin_value}
108+
return true, 'Converted mixin list to matcher hash to facilitate merging with configuration'
109+
end
110+
111+
# Promote config value list to all-matches matcher hash
112+
if config_value.is_a?(Array) && mixin_value.is_a?(Hash)
113+
# Ensure all-matches matcher key is a symbol and not a string
114+
mixin_value[:*] = value if value = mixin_value.delete( '*' )
115+
116+
# Replace the value of a simple array list with a matcher hash that stores the original list
117+
config_parent[key] = {:* => config_value}
118+
return true, 'Converted configuration list to matcher hash to facilitate merging with mixin'
119+
end
120+
121+
return false, nil
122+
end
123+
end

bin/mixinator.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
# SPDX-License-Identifier: MIT
66
# =========================================================================
77

8-
require 'deep_merge'
9-
108
class Mixinator
119

12-
constructor :path_validator, :yaml_wrapper, :loginator
10+
constructor :mixin_standardizer, :merginator, :path_validator, :yaml_wrapper, :loginator
1311

1412
def setup
15-
# ...
13+
# Aliases
14+
@standardinator = @mixin_standardizer
1615
end
1716

1817
def validate_cmdline_filepaths(paths)
@@ -96,7 +95,7 @@ def assemble_mixins(config:, env:, cmdline:)
9695
return assembly.reverse()
9796
end
9897

99-
def merge(builtins:, config:, mixins:)
98+
def mixin(builtins:, config:, mixins:)
10099
mixins.each do |mixin|
101100
source = mixin.keys.first
102101
filepath = mixin.values.first
@@ -124,8 +123,18 @@ def merge(builtins:, config:, mixins:)
124123
# Sanitize the mixin config by removing any :mixins section (these should not end up in merges)
125124
_mixin.delete(:mixins)
126125

127-
# Merge this bad boy
128-
config.deep_merge( _mixin )
126+
# Run special handling using knowledge of Ceedling configuration conventions
127+
notices = []
128+
if @standardinator.smart_standardize( config:config, mixin:_mixin, notices:notices )
129+
notices.each { |msg| @loginator.log( msg, Verbosity::COMPLAIN, LogLabels::NOTICE ) }
130+
end
131+
132+
warnings = []
133+
if !@merginator.merge( config:config, mixin:_mixin, warnings:warnings )
134+
msg = "Mixin values from #{filepath} will replace configuration values for incompatible merges..."
135+
@loginator.log( msg, Verbosity::COMPLAIN, LogLabels::NOTICE )
136+
warnings.each { |msg| @loginator.log( msg, Verbosity::COMPLAIN ) }
137+
end
129138
end
130139

131140
# Validate final configuration

bin/objects.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ system_wrapper:
2020

2121
verbosinator:
2222

23+
reportinator:
24+
2325
loginator:
2426
compose:
2527
- verbosinator
@@ -59,10 +61,20 @@ path_validator:
5961

6062
mixinator:
6163
compose:
64+
- mixin_standardizer
65+
- merginator
6266
- path_validator
6367
- yaml_wrapper
6468
- loginator
6569

70+
mixin_standardizer:
71+
compose:
72+
- reportinator
73+
74+
merginator:
75+
compose:
76+
- reportinator
77+
6678
projectinator:
6779
compose:
6880
- file_wrapper

0 commit comments

Comments
 (0)