diff --git a/README.md b/README.md index e994be1..6fad5a7 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,12 @@ Idea came from puppet's hiera. - [Vault](#vault) - [Merge with Terraform remote state](#merge-with-terraform-remote-state) - [Merge with env variables](#merge-with-env-variables) + - [Unicode Support](#unicode-support) - [himl config merger](#himl-config-merger) + - [Output filtering](#output-filtering) - [Extra merger features](#extra-merger-features) + - [Custom merge strategy](#custom-merge-strategy) + - [Development](#development) ## Installation @@ -151,8 +155,9 @@ usage: himl [-h] [--output-file OUTPUT_FILE] [--format OUTPUT_FORMAT] [--filter FILTER] [--exclude EXCLUDE] [--skip-interpolation-validation] [--skip-interpolation-resolving] [--enclosing-key ENCLOSING_KEY] - [--cwd CWD] + [--cwd CWD] [--multi-line-string] [--list-merge-strategy {append,override,prepend,append_unique}] + [--allow-unicode] path ``` @@ -296,6 +301,63 @@ endpoint: "{{outputs.cluster_composition.output.value.redis_endpoint}}" kubeconfig_location: "{{env(KUBECONFIG)}}" ``` +### Unicode Support + +himl supports Unicode characters in configuration files, allowing you to use international languages, special characters, and emoji in your YAML configs. + +By default, Unicode characters are escaped in the output to ensure compatibility. You can preserve Unicode characters in their original form using the `--allow-unicode` flag. + +**Using the CLI:** +```sh +# With Unicode escaping (default) +himl examples/simple/production --output-file config.yaml + +# Preserving Unicode characters +himl examples/simple/production --output-file config.yaml --allow-unicode +``` + +**Using the Python module:** +```py +from himl import ConfigProcessor + +config_processor = ConfigProcessor() +path = "examples/simple/production" + +# Process with Unicode preservation +config = config_processor.process( + path=path, + output_format="yaml", + allow_unicode=True, # Preserve Unicode characters + print_data=True +) +``` + +**Example with Unicode content:** + +`config/default.yaml`: +```yaml +service: + name: "My Service" + description: "Multi-language support: English, 中文, العربية, Русский" + +messages: + welcome: + en: "Welcome" + zh: "欢迎" + ar: "مرحبا" + ru: "Добро пожаловать" + +team: + - name: "José García" + role: "Developer" + - name: "田中太郎" + role: "Designer" +``` + +When processed with `--allow-unicode`, the output preserves all Unicode characters. Without the flag, non-ASCII characters are escaped (e.g., `\u4e2d\u6587` for Chinese characters). + +**Note:** Some emoji and 4-byte UTF-8 characters may be escaped by the YAML library even with `--allow-unicode` enabled. + ## himl-config-merger @@ -394,6 +456,12 @@ Build the output with filtering: himl-config-merger examples/filters --output-dir merged_output --levels env region cluster --leaf-directories cluster --filter-rules-key _filters ``` +The `himl-config-merger` command also supports the `--allow-unicode` flag for preserving Unicode characters in the merged output files: + +```sh +himl-config-merger examples/complex --output-dir merged_output --levels env region cluster --leaf-directories cluster --allow-unicode +``` + ```yaml # output after filtering env: dev diff --git a/himl/config_generator.py b/himl/config_generator.py index ae39c93..e3148ff 100755 --- a/himl/config_generator.py +++ b/himl/config_generator.py @@ -42,6 +42,7 @@ def process(self, cwd=None, skip_interpolation_validation=False, skip_secrets=False, multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"]): @@ -53,7 +54,7 @@ def process(self, cwd=None, cwd = cwd or os.getcwd() generator = self._create_and_initialize_generator( - cwd, path, multi_line_string, type_strategies, fallback_strategies, type_conflict_strategies) + cwd, path, multi_line_string, allow_unicode, type_strategies, fallback_strategies, type_conflict_strategies) # Process data exclusions and interpolations self._process_exclusions(generator, exclude_keys) @@ -73,10 +74,10 @@ def _should_skip_interpolation_validation(self, skip_interpolations, skip_secret """Determine if interpolation validation should be skipped.""" return skip_interpolation_validation or skip_interpolations or skip_secrets - def _create_and_initialize_generator(self, cwd, path, multi_line_string, type_strategies, + def _create_and_initialize_generator(self, cwd, path, multi_line_string, allow_unicode, type_strategies, fallback_strategies, type_conflict_strategies): """Create and initialize the ConfigGenerator.""" - generator = ConfigGenerator(cwd, path, multi_line_string, type_strategies, fallback_strategies, + generator = ConfigGenerator(cwd, path, multi_line_string, allow_unicode, type_strategies, fallback_strategies, type_conflict_strategies) generator.generate_hierarchy() generator.process_hierarchy() @@ -179,12 +180,13 @@ class ConfigGenerator(object): will contain merged data on each layer. """ - def __init__(self, cwd, path, multi_line_string, type_strategies, fallback_strategies, type_conflict_strategies): + def __init__(self, cwd, path, multi_line_string, allow_unicode, type_strategies, fallback_strategies, type_conflict_strategies): self.cwd = cwd self.path = path self.hierarchy = self.generate_hierarchy() self.generated_data = OrderedDict() self.interpolation_validator = InterpolationValidator() + self.allow_unicode = allow_unicode self.type_strategies = type_strategies self.fallback_strategies = fallback_strategies self.type_conflict_strategies = type_conflict_strategies @@ -338,7 +340,7 @@ def get_values_from_dir_path(self): def output_yaml_data(self, data): return yaml.dump(data, Dumper=ConfigGenerator.yaml_dumper(), default_flow_style=False, width=200, - sort_keys=False) + sort_keys=False, allow_unicode=self.allow_unicode) def yaml_to_json(self, yaml_data): return json.dumps(yaml.load(yaml_data, Loader=yaml.SafeLoader), indent=4) diff --git a/himl/config_merger.py b/himl/config_merger.py index b389305..6f00a3c 100755 --- a/himl/config_merger.py +++ b/himl/config_merger.py @@ -89,18 +89,19 @@ def __traverse_path(self, path: str, yaml_dict: dict): Loader.add_constructor('!include', Loader.include) -def merge_configs(directories, levels, output_dir, enable_parallel, filter_rules): +def merge_configs(directories, levels, output_dir, enable_parallel, filter_rules, allow_unicode): """ Method for running the merge configuration logic under different formats :param directories: list of paths for leaf directories :param levels: list of hierarchy levels to traverse :param output_dir: where to save the generated configs :param enable_parallel: to enable parallel config generation + :param allow_unicode: allow unicode characters in output """ config_processor = ConfigProcessor() process_config = [] for path in directories: - process_config.append((config_processor, path, levels, output_dir, filter_rules)) + process_config.append((config_processor, path, levels, output_dir, filter_rules, allow_unicode)) if enable_parallel: logger.info("Processing config in parallel") @@ -121,6 +122,7 @@ def merge_logic(process_params): levels = process_params[2] output_dir = process_params[3] filter_rules = process_params[4] + allow_unicode = process_params[5] # load the !include tag Loader.add_constructor('!include', Loader.include) @@ -153,7 +155,7 @@ def merge_logic(process_params): logger.info("Found input config directory: %s", path) logger.info("Storing generated config to: %s", filename) with open(filename, "w+") as f: - f.write(yaml.dump(output)) + f.write(yaml.dump(output, allow_unicode=allow_unicode)) def is_leaf_directory(dir, leaf_directories): @@ -203,6 +205,8 @@ def get_parser(): action='store_true', help='Process config using multiprocessing') parser.add_argument('--filter-rules-key', dest='filter_rules', default=None, type=str, help='keep these keys from the generated data, based on the configured filter key') + parser.add_argument('--allow-unicode', dest='allow_unicode', default=False, + action='store_true', help='allow unicode characters in output (default: False, outputs escape sequences)') return parser @@ -219,4 +223,4 @@ def run(args=None): # merge the configs using HIML merge_configs(dirs, opts.hierarchy_levels, - opts.output_dir, opts.enable_parallel, opts.filter_rules) + opts.output_dir, opts.enable_parallel, opts.filter_rules, opts.allow_unicode) diff --git a/himl/main.py b/himl/main.py index f1b7162..3d6b493 100644 --- a/himl/main.py +++ b/himl/main.py @@ -43,7 +43,7 @@ def do_run(self, opts): config_processor.process(cwd, opts.path, filters, excluded_keys, opts.enclosing_key, opts.remove_enclosing_key, opts.output_format, opts.print_data, opts.output_file, opts.skip_interpolation_resolving, opts.skip_interpolation_validation, - opts.skip_secrets, opts.multi_line_string, + opts.skip_secrets, opts.multi_line_string, opts.allow_unicode, type_strategies=[(list, [opts.merge_list_strategy.value]), (dict, ["merge"])]) @staticmethod @@ -79,6 +79,8 @@ def get_parser(parser=None): parser.add_argument('--list-merge-strategy', dest='merge_list_strategy', type=ListMergeStrategy, choices=list(ListMergeStrategy), default='append_unique', help='override default merge strategy for list') + parser.add_argument('--allow-unicode', dest='allow_unicode', action='store_true', default=False, + help='allow unicode characters in output (default: False, outputs escape sequences)') parser.add_argument('--version', action='version', version='%(prog)s v{version}'.format(version="0.18.0"), help='print himl version') return parser diff --git a/tests/test_config_generator.py b/tests/test_config_generator.py index 91bb3fa..604b889 100644 --- a/tests/test_config_generator.py +++ b/tests/test_config_generator.py @@ -191,6 +191,52 @@ def test_output_formats(self): ) assert json_result == config_data + def test_unicode_processing_disabled(self): + """Test Unicode processing with allow_unicode=False""" + config_data = { + 'message': 'Hello 世界', + 'emoji': '✨ sparkles', + 'accents': 'café' + } + self.create_test_yaml('unicode.yaml', config_data) + + result = self.config_processor.process( + cwd=self.temp_dir, + path='unicode.yaml', + allow_unicode=False, + print_data=False + ) + + # Data should be processed correctly regardless of Unicode settings + assert result['message'] == 'Hello 世界' + assert result['emoji'] == '✨ sparkles' + assert result['accents'] == 'café' + + def test_unicode_processing_enabled(self): + """Test Unicode processing with allow_unicode=True""" + config_data = { + 'message': 'Hello 世界', + 'emoji': '✨ sparkles', + 'accents': 'café', + 'arabic': 'مرحبا', + 'cyrillic': 'Привет' + } + self.create_test_yaml('unicode.yaml', config_data) + + result = self.config_processor.process( + cwd=self.temp_dir, + path='unicode.yaml', + allow_unicode=True, + print_data=False + ) + + # Data should be processed correctly + assert result['message'] == 'Hello 世界' + assert result['emoji'] == '✨ sparkles' + assert result['accents'] == 'café' + assert result['arabic'] == 'مرحبا' + assert result['cyrillic'] == 'Привет' + class TestConfigGenerator: """Test cases for ConfigGenerator class""" @@ -218,6 +264,7 @@ def test_config_generator_initialization(self): cwd=self.temp_dir, path='test', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -238,6 +285,7 @@ def test_hierarchy_generation(self): cwd=self.temp_dir, path='production', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -256,6 +304,7 @@ def test_yaml_content_loading(self): cwd=self.temp_dir, path='test', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -270,6 +319,7 @@ def test_yaml_merging(self): cwd=self.temp_dir, path='test', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -298,6 +348,7 @@ def test_output_data_yaml(self): cwd=self.temp_dir, path='test', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -316,6 +367,7 @@ def test_output_data_json(self): cwd=self.temp_dir, path='test', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -335,6 +387,7 @@ def test_invalid_output_format(self): cwd=self.temp_dir, path='test', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -353,6 +406,7 @@ def test_values_from_dir_path(self): cwd=self.temp_dir, path='env=production/region=us-east-1/cluster=web', multi_line_string=False, + allow_unicode=False, type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], fallback_strategies=["override"], type_conflict_strategies=["override"] @@ -361,3 +415,88 @@ def test_values_from_dir_path(self): values = generator.get_values_from_dir_path() expected = {'env': 'production', 'region': 'us-east-1', 'cluster': 'web'} assert values == expected + + def test_allow_unicode_false(self): + """Test that Unicode characters are escaped when allow_unicode=False""" + generator = ConfigGenerator( + cwd=self.temp_dir, + path='test', + multi_line_string=False, + allow_unicode=False, + type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], + fallback_strategies=["override"], + type_conflict_strategies=["override"] + ) + + test_data = { + 'greeting': 'Hello 世界', + 'emoji': '🚀 rocket', + 'special': 'café résumé naïve' + } + yaml_output = generator.output_yaml_data(test_data) + + # When allow_unicode=False, Unicode should be escaped + assert '\\u' in yaml_output or '\\x' in yaml_output or 'greeting: Hello' in yaml_output + + def test_allow_unicode_true(self): + """Test that Unicode characters are preserved when allow_unicode=True""" + generator = ConfigGenerator( + cwd=self.temp_dir, + path='test', + multi_line_string=False, + allow_unicode=True, + type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], + fallback_strategies=["override"], + type_conflict_strategies=["override"] + ) + + test_data = { + 'greeting': 'Hello 世界', + 'emoji': '🚀 rocket', + 'special': 'café résumé naïve' + } + yaml_output = generator.output_yaml_data(test_data) + + # When allow_unicode=True, most Unicode should be preserved + # Note: PyYAML may still escape some 4-byte UTF-8 characters (emojis) + assert '世界' in yaml_output # Chinese characters preserved + assert 'café' in yaml_output # Accented characters preserved + assert 'résumé' in yaml_output # Accented characters preserved + assert 'naïve' in yaml_output # Accented characters preserved + # Emoji might be escaped as \U0001F680 even with allow_unicode=True + assert ('🚀' in yaml_output or '\\U0001F680' in yaml_output) + + def test_unicode_in_nested_structures(self): + """Test Unicode handling in nested data structures""" + generator = ConfigGenerator( + cwd=self.temp_dir, + path='test', + multi_line_string=False, + allow_unicode=True, + type_strategies=[(list, ["append_unique"]), (dict, ["merge"])], + fallback_strategies=["override"], + type_conflict_strategies=["override"] + ) + + test_data = { + 'users': [ + {'name': 'José García', 'country': 'España'}, + {'name': '田中太郎', 'country': '日本'}, + {'name': 'François Müller', 'country': 'France'} + ], + 'config': { + 'title': 'Configuration — Настройки', + 'description': 'Multi-language support: English, 中文, العربية, हिन्दी' + } + } + yaml_output = generator.output_yaml_data(test_data) + + # Verify Unicode characters are preserved (excluding 4-byte emoji which may be escaped) + assert 'José García' in yaml_output + assert '田中太郎' in yaml_output + assert 'España' in yaml_output + assert '日本' in yaml_output + assert 'Настройки' in yaml_output + assert '中文' in yaml_output + assert 'العربية' in yaml_output + assert 'हिन्दी' in yaml_output diff --git a/tests/test_config_merger.py b/tests/test_config_merger.py index f936912..110d7d0 100644 --- a/tests/test_config_merger.py +++ b/tests/test_config_merger.py @@ -158,7 +158,8 @@ def test_merge_logic(self, mock_config_processor): os.path.join(self.temp_dir, 'env=dev/region=us-east-1/cluster=web'), ['env', 'region', 'cluster'], output_dir, - None # No filter rules + None, # No filter rules + False # allow_unicode ) merge_logic(config_tuple) @@ -203,7 +204,8 @@ def test_merge_logic_with_filters(self, mock_filter_rules, mock_config_processor os.path.join(self.temp_dir, 'env=dev/region=us-east-1/cluster=web'), ['env', 'region', 'cluster'], output_dir, - '_filters' + '_filters', + False # allow_unicode ) merge_logic(config_tuple) @@ -224,7 +226,7 @@ def test_merge_configs_parallel(self, mock_cpu_count, mock_pool): levels = ['env', 'region'] output_dir = '/output' - merge_configs(directories, levels, output_dir, enable_parallel=True, filter_rules=None) + merge_configs(directories, levels, output_dir, enable_parallel=True, filter_rules=None, allow_unicode=False) mock_pool.assert_called_once_with(4) mock_pool_instance.map.assert_called_once() @@ -236,7 +238,7 @@ def test_merge_configs_sequential(self, mock_merge_logic): levels = ['env', 'region'] output_dir = '/output' - merge_configs(directories, levels, output_dir, enable_parallel=False, filter_rules=None) + merge_configs(directories, levels, output_dir, enable_parallel=False, filter_rules=None, allow_unicode=False) assert mock_merge_logic.call_count == 2 @@ -287,7 +289,8 @@ def test_run_function(self, mock_get_leaf_directories, mock_merge_configs): ['env', 'region'], 'output', False, # enable_parallel default - None # filter_rules_key default + None, # filter_rules_key default + False # allow_unicode default ) def test_parser_default_values(self): @@ -348,10 +351,106 @@ def test_merge_logic_missing_filter_key(self, mock_config_processor): os.path.join(self.temp_dir, 'env=dev/region=us-east-1/cluster=web'), ['env', 'region', 'cluster'], output_dir, - '_filters' # Filter key that doesn't exist + '_filters', # Filter key that doesn't exist + False # allow_unicode ) with pytest.raises(Exception) as exc_info: merge_logic(config_tuple) assert "Filter rule key '_filters' not found in config" in str(exc_info.value) + + def test_parser_allow_unicode_flag(self): + """Test allow-unicode flag parsing""" + parser = get_parser() + + # Test default value (False) + args = parser.parse_args([ + 'input_dir', + '--output-dir', 'output', + '--levels', 'env', + '--leaf-directories', 'cluster' + ]) + assert args.allow_unicode is False + + # Test when flag is set (True) + args = parser.parse_args([ + 'input_dir', + '--output-dir', 'output', + '--levels', 'env', + '--leaf-directories', 'cluster', + '--allow-unicode' + ]) + assert args.allow_unicode is True + + @patch('himl.config_merger.ConfigProcessor') + def test_merge_logic_with_unicode(self, mock_config_processor): + """Test merge logic with Unicode content""" + self.create_directory_structure() + + # Mock ConfigProcessor with Unicode content + mock_processor = MagicMock() + mock_processor.process.return_value = { + 'env': 'dev', + 'region': 'us-east-1', + 'cluster': 'web', + 'message': 'Hello 世界', + 'emoji': '🚀 rocket', + 'multilingual': { + 'japanese': 'こんにちは', + 'arabic': 'مرحبا', + 'russian': 'Привет' + } + } + + output_dir = os.path.join(self.temp_dir, 'output') + os.makedirs(output_dir, exist_ok=True) + + # Test with allow_unicode=True + config_tuple = ( + mock_processor, + os.path.join(self.temp_dir, 'env=dev/region=us-east-1/cluster=web'), + ['env', 'region', 'cluster'], + output_dir, + None, + True # allow_unicode=True + ) + + merge_logic(config_tuple) + + # Verify output file was created + expected_output = os.path.join(output_dir, 'dev/us-east-1/web.yaml') + assert os.path.exists(expected_output) + + # Verify content has Unicode characters + with open(expected_output, 'r', encoding='utf-8') as f: + content = f.read() + # Check that Unicode is preserved in the file + assert '世界' in content or 'message:' in content # Unicode should be present or at least the key + + @patch('himl.config_merger.merge_configs') + @patch('himl.config_merger.get_leaf_directories') + def test_run_with_unicode_flag(self, mock_get_leaf_directories, mock_merge_configs): + """Test run function with allow-unicode flag""" + mock_get_leaf_directories.return_value = ['dir1', 'dir2'] + + args = [ + 'input_dir', + '--output-dir', 'output', + '--levels', 'env', 'region', + '--leaf-directories', 'cluster', + '--allow-unicode' + ] + + with patch('sys.argv', ['himl-config-merger'] + args): + run() + + mock_get_leaf_directories.assert_called_once_with('input_dir', ['cluster']) + mock_merge_configs.assert_called_once_with( + ['dir1', 'dir2'], + ['env', 'region'], + 'output', + False, # enable_parallel default + None, # filter_rules_key default + True # allow_unicode=True + ) diff --git a/tests/test_main.py b/tests/test_main.py index 75cc490..334b2d4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -89,6 +89,7 @@ def test_do_run_basic(self, mock_config_processor): mock_opts.skip_interpolation_validation = False mock_opts.skip_secrets = False mock_opts.multi_line_string = False + mock_opts.allow_unicode = False mock_opts.merge_list_strategy = MagicMock() mock_opts.merge_list_strategy.value = 'append_unique' @@ -115,6 +116,7 @@ def test_do_run_basic(self, mock_config_processor): False, False, False, + False, type_strategies=[(list, ['append_unique']), (dict, ["merge"])] ) @@ -135,6 +137,7 @@ def test_do_run_with_filters(self, mock_config_processor): mock_opts.skip_interpolation_validation = True mock_opts.skip_secrets = True mock_opts.multi_line_string = True + mock_opts.allow_unicode = False mock_opts.merge_list_strategy = MagicMock() mock_opts.merge_list_strategy.value = 'override' @@ -157,6 +160,7 @@ def test_do_run_with_filters(self, mock_config_processor): True, True, True, + False, type_strategies=[(list, ['override']), (dict, ["merge"])] ) @@ -265,6 +269,7 @@ def test_output_file_sets_print_data_false(self, mock_config_processor): mock_opts.skip_interpolation_validation = False mock_opts.skip_secrets = False mock_opts.multi_line_string = False + mock_opts.allow_unicode = False mock_opts.merge_list_strategy = MagicMock() mock_opts.merge_list_strategy.value = 'append_unique' @@ -307,6 +312,7 @@ def test_empty_filters_and_excludes(self, mock_config_processor): mock_opts.skip_interpolation_validation = False mock_opts.skip_secrets = False mock_opts.multi_line_string = False + mock_opts.allow_unicode = False mock_opts.merge_list_strategy = MagicMock() mock_opts.merge_list_strategy.value = 'append_unique' @@ -322,3 +328,74 @@ def test_empty_filters_and_excludes(self, mock_config_processor): assert call_args[2] == () # exclude_keys is the 4th positional argument (index 3) assert call_args[3] == () + + def test_parser_allow_unicode_flag(self): + """Test allow-unicode flag parsing""" + parser = self.runner.get_parser() + + # Test default value (False) + args = parser.parse_args(['test_path']) + assert args.allow_unicode is False + + # Test when flag is set (True) + args = parser.parse_args(['test_path', '--allow-unicode']) + assert args.allow_unicode is True + + @patch('himl.main.ConfigProcessor') + def test_do_run_with_allow_unicode_true(self, mock_config_processor): + """Test do_run with allow_unicode=True""" + mock_opts = MagicMock() + mock_opts.cwd = None + mock_opts.path = 'test_path' + mock_opts.filter = None + mock_opts.exclude = None + mock_opts.output_file = None + mock_opts.print_data = True + mock_opts.output_format = 'yaml' + mock_opts.enclosing_key = None + mock_opts.remove_enclosing_key = None + mock_opts.skip_interpolation_resolving = False + mock_opts.skip_interpolation_validation = False + mock_opts.skip_secrets = False + mock_opts.multi_line_string = False + mock_opts.allow_unicode = True # Enable Unicode + mock_opts.merge_list_strategy = MagicMock() + mock_opts.merge_list_strategy.value = 'append_unique' + + mock_processor_instance = MagicMock() + mock_config_processor.return_value = mock_processor_instance + + with patch('os.getcwd', return_value='/current/dir'): + self.runner.do_run(mock_opts) + + # Verify allow_unicode=True is passed correctly + call_args = mock_processor_instance.process.call_args[0] + # allow_unicode is the 14th positional argument (index 13) + assert call_args[13] is True + + @patch('sys.stdout', new_callable=StringIO) + def test_run_integration_with_unicode(self, mock_stdout): + """Test integration with Unicode content""" + # Create test config with Unicode content + test_data = { + 'greeting': 'Hello 世界', + 'emoji': '🚀 rocket', + 'multilingual': { + 'english': 'Hello', + 'japanese': 'こんにちは', + 'arabic': 'مرحبا' + } + } + self.create_test_yaml('unicode_config.yaml', test_data) + + args = [ + os.path.join(self.temp_dir, 'unicode_config.yaml'), + '--allow-unicode' + ] + + with patch('os.getcwd', return_value=self.temp_dir): + self.runner.run(args) + + # Verify output contains Unicode characters + output = mock_stdout.getvalue() + assert 'greeting' in output or 'Hello' in output # Basic verification that something was output