machine-dialect 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- machine_dialect/__main__.py +667 -0
- machine_dialect/agent/__init__.py +5 -0
- machine_dialect/agent/agent.py +360 -0
- machine_dialect/ast/__init__.py +95 -0
- machine_dialect/ast/ast_node.py +35 -0
- machine_dialect/ast/call_expression.py +82 -0
- machine_dialect/ast/dict_extraction.py +60 -0
- machine_dialect/ast/expressions.py +439 -0
- machine_dialect/ast/literals.py +309 -0
- machine_dialect/ast/program.py +35 -0
- machine_dialect/ast/statements.py +1433 -0
- machine_dialect/ast/tests/test_ast_string_representation.py +62 -0
- machine_dialect/ast/tests/test_boolean_literal.py +29 -0
- machine_dialect/ast/tests/test_collection_hir.py +138 -0
- machine_dialect/ast/tests/test_define_statement.py +142 -0
- machine_dialect/ast/tests/test_desugar.py +541 -0
- machine_dialect/ast/tests/test_foreach_desugar.py +245 -0
- machine_dialect/cfg/__init__.py +6 -0
- machine_dialect/cfg/config.py +156 -0
- machine_dialect/cfg/examples.py +221 -0
- machine_dialect/cfg/generate_with_ai.py +187 -0
- machine_dialect/cfg/openai_generation.py +200 -0
- machine_dialect/cfg/parser.py +94 -0
- machine_dialect/cfg/tests/__init__.py +1 -0
- machine_dialect/cfg/tests/test_cfg_parser.py +252 -0
- machine_dialect/cfg/tests/test_config.py +188 -0
- machine_dialect/cfg/tests/test_examples.py +391 -0
- machine_dialect/cfg/tests/test_generate_with_ai.py +354 -0
- machine_dialect/cfg/tests/test_openai_generation.py +256 -0
- machine_dialect/codegen/__init__.py +5 -0
- machine_dialect/codegen/bytecode_module.py +89 -0
- machine_dialect/codegen/bytecode_serializer.py +300 -0
- machine_dialect/codegen/opcodes.py +101 -0
- machine_dialect/codegen/register_codegen.py +1996 -0
- machine_dialect/codegen/symtab.py +208 -0
- machine_dialect/codegen/tests/__init__.py +1 -0
- machine_dialect/codegen/tests/test_array_operations_codegen.py +295 -0
- machine_dialect/codegen/tests/test_bytecode_serializer.py +185 -0
- machine_dialect/codegen/tests/test_register_codegen_ssa.py +324 -0
- machine_dialect/codegen/tests/test_symtab.py +418 -0
- machine_dialect/codegen/vm_serializer.py +621 -0
- machine_dialect/compiler/__init__.py +18 -0
- machine_dialect/compiler/compiler.py +197 -0
- machine_dialect/compiler/config.py +149 -0
- machine_dialect/compiler/context.py +149 -0
- machine_dialect/compiler/phases/__init__.py +19 -0
- machine_dialect/compiler/phases/bytecode_optimization.py +90 -0
- machine_dialect/compiler/phases/codegen.py +40 -0
- machine_dialect/compiler/phases/hir_generation.py +39 -0
- machine_dialect/compiler/phases/mir_generation.py +86 -0
- machine_dialect/compiler/phases/optimization.py +110 -0
- machine_dialect/compiler/phases/parsing.py +39 -0
- machine_dialect/compiler/pipeline.py +143 -0
- machine_dialect/compiler/tests/__init__.py +1 -0
- machine_dialect/compiler/tests/test_compiler.py +568 -0
- machine_dialect/compiler/vm_runner.py +173 -0
- machine_dialect/errors/__init__.py +32 -0
- machine_dialect/errors/exceptions.py +369 -0
- machine_dialect/errors/messages.py +82 -0
- machine_dialect/errors/tests/__init__.py +0 -0
- machine_dialect/errors/tests/test_expected_token_errors.py +188 -0
- machine_dialect/errors/tests/test_name_errors.py +118 -0
- machine_dialect/helpers/__init__.py +0 -0
- machine_dialect/helpers/stopwords.py +225 -0
- machine_dialect/helpers/validators.py +30 -0
- machine_dialect/lexer/__init__.py +9 -0
- machine_dialect/lexer/constants.py +23 -0
- machine_dialect/lexer/lexer.py +907 -0
- machine_dialect/lexer/tests/__init__.py +0 -0
- machine_dialect/lexer/tests/helpers.py +86 -0
- machine_dialect/lexer/tests/test_apostrophe_identifiers.py +122 -0
- machine_dialect/lexer/tests/test_backtick_identifiers.py +140 -0
- machine_dialect/lexer/tests/test_boolean_literals.py +108 -0
- machine_dialect/lexer/tests/test_case_insensitive_keywords.py +188 -0
- machine_dialect/lexer/tests/test_comments.py +200 -0
- machine_dialect/lexer/tests/test_double_asterisk_keywords.py +127 -0
- machine_dialect/lexer/tests/test_lexer_position.py +113 -0
- machine_dialect/lexer/tests/test_list_tokens.py +282 -0
- machine_dialect/lexer/tests/test_stopwords.py +80 -0
- machine_dialect/lexer/tests/test_strict_equality.py +129 -0
- machine_dialect/lexer/tests/test_token.py +41 -0
- machine_dialect/lexer/tests/test_tokenization.py +294 -0
- machine_dialect/lexer/tests/test_underscore_literals.py +343 -0
- machine_dialect/lexer/tests/test_url_literals.py +169 -0
- machine_dialect/lexer/tokens.py +487 -0
- machine_dialect/linter/__init__.py +10 -0
- machine_dialect/linter/__main__.py +144 -0
- machine_dialect/linter/linter.py +154 -0
- machine_dialect/linter/rules/__init__.py +8 -0
- machine_dialect/linter/rules/base.py +112 -0
- machine_dialect/linter/rules/statement_termination.py +99 -0
- machine_dialect/linter/tests/__init__.py +1 -0
- machine_dialect/linter/tests/mdrules/__init__.py +0 -0
- machine_dialect/linter/tests/mdrules/test_md101_statement_termination.py +181 -0
- machine_dialect/linter/tests/test_linter.py +81 -0
- machine_dialect/linter/tests/test_rules.py +110 -0
- machine_dialect/linter/tests/test_violations.py +71 -0
- machine_dialect/linter/violations.py +51 -0
- machine_dialect/mir/__init__.py +69 -0
- machine_dialect/mir/analyses/__init__.py +20 -0
- machine_dialect/mir/analyses/alias_analysis.py +315 -0
- machine_dialect/mir/analyses/dominance_analysis.py +49 -0
- machine_dialect/mir/analyses/escape_analysis.py +286 -0
- machine_dialect/mir/analyses/loop_analysis.py +272 -0
- machine_dialect/mir/analyses/tests/test_type_analysis.py +736 -0
- machine_dialect/mir/analyses/type_analysis.py +448 -0
- machine_dialect/mir/analyses/use_def_chains.py +232 -0
- machine_dialect/mir/basic_block.py +385 -0
- machine_dialect/mir/dataflow.py +445 -0
- machine_dialect/mir/debug_info.py +208 -0
- machine_dialect/mir/hir_to_mir.py +1738 -0
- machine_dialect/mir/mir_dumper.py +366 -0
- machine_dialect/mir/mir_function.py +167 -0
- machine_dialect/mir/mir_instructions.py +1877 -0
- machine_dialect/mir/mir_interpreter.py +556 -0
- machine_dialect/mir/mir_module.py +225 -0
- machine_dialect/mir/mir_printer.py +480 -0
- machine_dialect/mir/mir_transformer.py +410 -0
- machine_dialect/mir/mir_types.py +367 -0
- machine_dialect/mir/mir_validation.py +455 -0
- machine_dialect/mir/mir_values.py +268 -0
- machine_dialect/mir/optimization_config.py +233 -0
- machine_dialect/mir/optimization_pass.py +251 -0
- machine_dialect/mir/optimization_pipeline.py +355 -0
- machine_dialect/mir/optimizations/__init__.py +84 -0
- machine_dialect/mir/optimizations/algebraic_simplification.py +733 -0
- machine_dialect/mir/optimizations/branch_prediction.py +372 -0
- machine_dialect/mir/optimizations/constant_propagation.py +634 -0
- machine_dialect/mir/optimizations/cse.py +398 -0
- machine_dialect/mir/optimizations/dce.py +288 -0
- machine_dialect/mir/optimizations/inlining.py +551 -0
- machine_dialect/mir/optimizations/jump_threading.py +487 -0
- machine_dialect/mir/optimizations/licm.py +405 -0
- machine_dialect/mir/optimizations/loop_unrolling.py +366 -0
- machine_dialect/mir/optimizations/strength_reduction.py +422 -0
- machine_dialect/mir/optimizations/tail_call.py +207 -0
- machine_dialect/mir/optimizations/tests/test_loop_unrolling.py +483 -0
- machine_dialect/mir/optimizations/type_narrowing.py +397 -0
- machine_dialect/mir/optimizations/type_specialization.py +447 -0
- machine_dialect/mir/optimizations/type_specific.py +906 -0
- machine_dialect/mir/optimize_mir.py +89 -0
- machine_dialect/mir/pass_manager.py +391 -0
- machine_dialect/mir/profiling/__init__.py +26 -0
- machine_dialect/mir/profiling/profile_collector.py +318 -0
- machine_dialect/mir/profiling/profile_data.py +372 -0
- machine_dialect/mir/profiling/profile_reader.py +272 -0
- machine_dialect/mir/profiling/profile_writer.py +226 -0
- machine_dialect/mir/register_allocation.py +302 -0
- machine_dialect/mir/reporting/__init__.py +17 -0
- machine_dialect/mir/reporting/optimization_reporter.py +314 -0
- machine_dialect/mir/reporting/report_formatter.py +289 -0
- machine_dialect/mir/ssa_construction.py +342 -0
- machine_dialect/mir/tests/__init__.py +1 -0
- machine_dialect/mir/tests/test_algebraic_associativity.py +204 -0
- machine_dialect/mir/tests/test_algebraic_complex_patterns.py +221 -0
- machine_dialect/mir/tests/test_algebraic_division.py +126 -0
- machine_dialect/mir/tests/test_algebraic_simplification.py +863 -0
- machine_dialect/mir/tests/test_basic_block.py +425 -0
- machine_dialect/mir/tests/test_branch_prediction.py +459 -0
- machine_dialect/mir/tests/test_call_lowering.py +168 -0
- machine_dialect/mir/tests/test_collection_lowering.py +604 -0
- machine_dialect/mir/tests/test_cross_block_constant_propagation.py +255 -0
- machine_dialect/mir/tests/test_custom_passes.py +166 -0
- machine_dialect/mir/tests/test_debug_info.py +285 -0
- machine_dialect/mir/tests/test_dict_extraction_lowering.py +192 -0
- machine_dialect/mir/tests/test_dictionary_lowering.py +299 -0
- machine_dialect/mir/tests/test_double_negation.py +231 -0
- machine_dialect/mir/tests/test_escape_analysis.py +233 -0
- machine_dialect/mir/tests/test_hir_to_mir.py +465 -0
- machine_dialect/mir/tests/test_hir_to_mir_complete.py +389 -0
- machine_dialect/mir/tests/test_hir_to_mir_simple.py +130 -0
- machine_dialect/mir/tests/test_inlining.py +435 -0
- machine_dialect/mir/tests/test_licm.py +472 -0
- machine_dialect/mir/tests/test_mir_dumper.py +313 -0
- machine_dialect/mir/tests/test_mir_instructions.py +445 -0
- machine_dialect/mir/tests/test_mir_module.py +860 -0
- machine_dialect/mir/tests/test_mir_printer.py +387 -0
- machine_dialect/mir/tests/test_mir_types.py +123 -0
- machine_dialect/mir/tests/test_mir_types_enhanced.py +132 -0
- machine_dialect/mir/tests/test_mir_validation.py +378 -0
- machine_dialect/mir/tests/test_mir_values.py +168 -0
- machine_dialect/mir/tests/test_one_based_indexing.py +202 -0
- machine_dialect/mir/tests/test_optimization_helpers.py +60 -0
- machine_dialect/mir/tests/test_optimization_pipeline.py +554 -0
- machine_dialect/mir/tests/test_optimization_reporter.py +318 -0
- machine_dialect/mir/tests/test_pass_manager.py +294 -0
- machine_dialect/mir/tests/test_pass_registration.py +64 -0
- machine_dialect/mir/tests/test_profiling.py +356 -0
- machine_dialect/mir/tests/test_register_allocation.py +307 -0
- machine_dialect/mir/tests/test_report_formatters.py +372 -0
- machine_dialect/mir/tests/test_ssa_construction.py +433 -0
- machine_dialect/mir/tests/test_tail_call.py +236 -0
- machine_dialect/mir/tests/test_type_annotated_instructions.py +192 -0
- machine_dialect/mir/tests/test_type_narrowing.py +277 -0
- machine_dialect/mir/tests/test_type_specialization.py +421 -0
- machine_dialect/mir/tests/test_type_specific_optimization.py +545 -0
- machine_dialect/mir/tests/test_type_specific_optimization_advanced.py +382 -0
- machine_dialect/mir/type_inference.py +368 -0
- machine_dialect/parser/__init__.py +12 -0
- machine_dialect/parser/enums.py +45 -0
- machine_dialect/parser/parser.py +3655 -0
- machine_dialect/parser/protocols.py +11 -0
- machine_dialect/parser/symbol_table.py +169 -0
- machine_dialect/parser/tests/__init__.py +0 -0
- machine_dialect/parser/tests/helper_functions.py +193 -0
- machine_dialect/parser/tests/test_action_statements.py +334 -0
- machine_dialect/parser/tests/test_boolean_literal_expressions.py +152 -0
- machine_dialect/parser/tests/test_call_statements.py +154 -0
- machine_dialect/parser/tests/test_call_statements_errors.py +187 -0
- machine_dialect/parser/tests/test_collection_mutations.py +264 -0
- machine_dialect/parser/tests/test_conditional_expressions.py +343 -0
- machine_dialect/parser/tests/test_define_integration.py +468 -0
- machine_dialect/parser/tests/test_define_statements.py +311 -0
- machine_dialect/parser/tests/test_dict_extraction.py +115 -0
- machine_dialect/parser/tests/test_empty_literal.py +155 -0
- machine_dialect/parser/tests/test_float_literal_expressions.py +163 -0
- machine_dialect/parser/tests/test_identifier_expressions.py +57 -0
- machine_dialect/parser/tests/test_if_empty_block.py +61 -0
- machine_dialect/parser/tests/test_if_statements.py +299 -0
- machine_dialect/parser/tests/test_illegal_tokens.py +86 -0
- machine_dialect/parser/tests/test_infix_expressions.py +680 -0
- machine_dialect/parser/tests/test_integer_literal_expressions.py +137 -0
- machine_dialect/parser/tests/test_interaction_statements.py +269 -0
- machine_dialect/parser/tests/test_list_literals.py +277 -0
- machine_dialect/parser/tests/test_no_none_in_ast.py +94 -0
- machine_dialect/parser/tests/test_panic_mode_recovery.py +171 -0
- machine_dialect/parser/tests/test_parse_errors.py +114 -0
- machine_dialect/parser/tests/test_possessive_syntax.py +182 -0
- machine_dialect/parser/tests/test_prefix_expressions.py +415 -0
- machine_dialect/parser/tests/test_program.py +13 -0
- machine_dialect/parser/tests/test_return_statements.py +89 -0
- machine_dialect/parser/tests/test_set_statements.py +152 -0
- machine_dialect/parser/tests/test_strict_equality.py +258 -0
- machine_dialect/parser/tests/test_symbol_table.py +217 -0
- machine_dialect/parser/tests/test_url_literal_expressions.py +209 -0
- machine_dialect/parser/tests/test_utility_statements.py +423 -0
- machine_dialect/parser/token_buffer.py +159 -0
- machine_dialect/repl/__init__.py +3 -0
- machine_dialect/repl/repl.py +426 -0
- machine_dialect/repl/tests/__init__.py +0 -0
- machine_dialect/repl/tests/test_repl.py +606 -0
- machine_dialect/semantic/__init__.py +12 -0
- machine_dialect/semantic/analyzer.py +906 -0
- machine_dialect/semantic/error_messages.py +189 -0
- machine_dialect/semantic/tests/__init__.py +1 -0
- machine_dialect/semantic/tests/test_analyzer.py +364 -0
- machine_dialect/semantic/tests/test_error_messages.py +104 -0
- machine_dialect/tests/edge_cases/__init__.py +10 -0
- machine_dialect/tests/edge_cases/test_boundary_access.py +256 -0
- machine_dialect/tests/edge_cases/test_empty_collections.py +166 -0
- machine_dialect/tests/edge_cases/test_invalid_operations.py +243 -0
- machine_dialect/tests/edge_cases/test_named_list_edge_cases.py +295 -0
- machine_dialect/tests/edge_cases/test_nested_structures.py +313 -0
- machine_dialect/tests/edge_cases/test_type_mixing.py +277 -0
- machine_dialect/tests/integration/test_array_operations_emulation.py +248 -0
- machine_dialect/tests/integration/test_list_compilation.py +395 -0
- machine_dialect/tests/integration/test_lists_and_dictionaries.py +322 -0
- machine_dialect/type_checking/__init__.py +21 -0
- machine_dialect/type_checking/tests/__init__.py +1 -0
- machine_dialect/type_checking/tests/test_type_system.py +230 -0
- machine_dialect/type_checking/type_system.py +270 -0
- machine_dialect-0.1.0a1.dist-info/METADATA +128 -0
- machine_dialect-0.1.0a1.dist-info/RECORD +268 -0
- machine_dialect-0.1.0a1.dist-info/WHEEL +5 -0
- machine_dialect-0.1.0a1.dist-info/entry_points.txt +3 -0
- machine_dialect-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- machine_dialect-0.1.0a1.dist-info/top_level.txt +2 -0
- machine_dialect_vm/__init__.pyi +15 -0
@@ -0,0 +1,354 @@
|
|
1
|
+
"""Tests for the AI-based Machine Dialect™ code generation module."""
|
2
|
+
|
3
|
+
from unittest.mock import MagicMock, Mock, mock_open, patch
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
from machine_dialect.cfg.generate_with_ai import generate_code, main
|
8
|
+
|
9
|
+
|
10
|
+
class TestGenerateCode:
|
11
|
+
"""Test the generate_code function."""
|
12
|
+
|
13
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
14
|
+
@patch("machine_dialect.cfg.generate_with_ai.CFGParser")
|
15
|
+
def test_generate_code_with_valid_config(self, mock_parser_class: Mock, mock_loader_class: Mock) -> None:
|
16
|
+
"""Test successful code generation with valid configuration."""
|
17
|
+
# Setup mocks
|
18
|
+
mock_loader = mock_loader_class.return_value
|
19
|
+
mock_config = MagicMock()
|
20
|
+
mock_config.key = "test-api-key"
|
21
|
+
mock_config.model = "gpt-3.5-turbo"
|
22
|
+
mock_loader.load.return_value = mock_config
|
23
|
+
|
24
|
+
mock_parser = mock_parser_class.return_value
|
25
|
+
mock_parser.validate.return_value = True
|
26
|
+
|
27
|
+
# Call function
|
28
|
+
result = generate_code(
|
29
|
+
task="calculate area",
|
30
|
+
temperature=0.5,
|
31
|
+
max_tokens=300,
|
32
|
+
validate=True,
|
33
|
+
)
|
34
|
+
|
35
|
+
# Verify result contains example code
|
36
|
+
assert "Set `width` to" in result
|
37
|
+
assert "Set `height` to" in result
|
38
|
+
assert "Set `area` to" in result
|
39
|
+
|
40
|
+
# Verify mocks were called
|
41
|
+
mock_loader.load.assert_called_once()
|
42
|
+
mock_parser.validate.assert_called_once()
|
43
|
+
|
44
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
45
|
+
@patch("machine_dialect.cfg.generate_with_ai.CFGParser")
|
46
|
+
def test_generate_code_with_api_key_override(self, mock_parser_class: Mock, mock_loader_class: Mock) -> None:
|
47
|
+
"""Test that API key parameter overrides config."""
|
48
|
+
# Setup mocks
|
49
|
+
mock_loader = mock_loader_class.return_value
|
50
|
+
mock_config = MagicMock()
|
51
|
+
mock_config.key = "config-api-key"
|
52
|
+
mock_config.model = "gpt-3.5-turbo"
|
53
|
+
mock_loader.load.return_value = mock_config
|
54
|
+
|
55
|
+
mock_parser = mock_parser_class.return_value
|
56
|
+
mock_parser.validate.return_value = True
|
57
|
+
|
58
|
+
# Call with API key override
|
59
|
+
result = generate_code(
|
60
|
+
task="test task",
|
61
|
+
api_key="override-api-key",
|
62
|
+
validate=True,
|
63
|
+
)
|
64
|
+
|
65
|
+
# Verify the config was overridden
|
66
|
+
assert mock_config.key == "override-api-key"
|
67
|
+
assert result is not None
|
68
|
+
|
69
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
70
|
+
@patch("machine_dialect.cfg.generate_with_ai.CFGParser")
|
71
|
+
def test_generate_code_with_model_override(self, mock_parser_class: Mock, mock_loader_class: Mock) -> None:
|
72
|
+
"""Test that model parameter overrides config."""
|
73
|
+
# Setup mocks
|
74
|
+
mock_loader = mock_loader_class.return_value
|
75
|
+
mock_config = MagicMock()
|
76
|
+
mock_config.key = "test-api-key"
|
77
|
+
mock_config.model = "gpt-3.5-turbo"
|
78
|
+
mock_loader.load.return_value = mock_config
|
79
|
+
|
80
|
+
mock_parser = mock_parser_class.return_value
|
81
|
+
mock_parser.validate.return_value = True
|
82
|
+
|
83
|
+
# Call with model override
|
84
|
+
result = generate_code(
|
85
|
+
task="test task",
|
86
|
+
model="gpt-4",
|
87
|
+
validate=True,
|
88
|
+
)
|
89
|
+
|
90
|
+
# Verify the config was overridden
|
91
|
+
assert mock_config.model == "gpt-4"
|
92
|
+
assert result is not None
|
93
|
+
|
94
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
95
|
+
def test_generate_code_missing_api_key(self, mock_loader_class: Mock) -> None:
|
96
|
+
"""Test error when API key is not configured."""
|
97
|
+
# Setup mocks - no API key
|
98
|
+
mock_loader = mock_loader_class.return_value
|
99
|
+
mock_config = MagicMock()
|
100
|
+
mock_config.key = None
|
101
|
+
mock_config.model = "gpt-3.5-turbo"
|
102
|
+
mock_loader.load.return_value = mock_config
|
103
|
+
mock_loader.get_error_message.return_value = "Please configure API key"
|
104
|
+
|
105
|
+
# Should raise ValueError
|
106
|
+
with pytest.raises(ValueError, match="Please configure API key"):
|
107
|
+
generate_code(task="test task")
|
108
|
+
|
109
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
110
|
+
def test_generate_code_missing_model(self, mock_loader_class: Mock) -> None:
|
111
|
+
"""Test error when model is not configured."""
|
112
|
+
# Setup mocks - no model
|
113
|
+
mock_loader = mock_loader_class.return_value
|
114
|
+
mock_config = MagicMock()
|
115
|
+
mock_config.key = "test-api-key"
|
116
|
+
mock_config.model = None
|
117
|
+
mock_loader.load.return_value = mock_config
|
118
|
+
mock_loader.get_error_message.return_value = "Please configure model"
|
119
|
+
|
120
|
+
# Should raise ValueError
|
121
|
+
with pytest.raises(ValueError, match="No AI model configured"):
|
122
|
+
generate_code(task="test task")
|
123
|
+
|
124
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
125
|
+
@patch("machine_dialect.cfg.generate_with_ai.CFGParser")
|
126
|
+
@patch("builtins.print")
|
127
|
+
def test_generate_code_without_validation(
|
128
|
+
self, mock_print: Mock, mock_parser_class: Mock, mock_loader_class: Mock
|
129
|
+
) -> None:
|
130
|
+
"""Test code generation without validation."""
|
131
|
+
# Setup mocks
|
132
|
+
mock_loader = mock_loader_class.return_value
|
133
|
+
mock_config = MagicMock()
|
134
|
+
mock_config.key = "test-api-key"
|
135
|
+
mock_config.model = "gpt-3.5-turbo"
|
136
|
+
mock_loader.load.return_value = mock_config
|
137
|
+
|
138
|
+
mock_parser = mock_parser_class.return_value
|
139
|
+
|
140
|
+
# Call without validation
|
141
|
+
result = generate_code(
|
142
|
+
task="test task",
|
143
|
+
validate=False,
|
144
|
+
)
|
145
|
+
|
146
|
+
# Verify parser was not instantiated/called
|
147
|
+
mock_parser.validate.assert_not_called()
|
148
|
+
assert result is not None
|
149
|
+
|
150
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
151
|
+
@patch("machine_dialect.cfg.generate_with_ai.CFGParser")
|
152
|
+
@patch("builtins.print")
|
153
|
+
def test_generate_code_with_invalid_syntax(
|
154
|
+
self, mock_print: Mock, mock_parser_class: Mock, mock_loader_class: Mock
|
155
|
+
) -> None:
|
156
|
+
"""Test code generation when validation fails."""
|
157
|
+
# Setup mocks
|
158
|
+
mock_loader = mock_loader_class.return_value
|
159
|
+
mock_config = MagicMock()
|
160
|
+
mock_config.key = "test-api-key"
|
161
|
+
mock_config.model = "gpt-3.5-turbo"
|
162
|
+
mock_loader.load.return_value = mock_config
|
163
|
+
|
164
|
+
mock_parser = mock_parser_class.return_value
|
165
|
+
mock_parser.validate.return_value = False
|
166
|
+
|
167
|
+
# Call with validation
|
168
|
+
result = generate_code(
|
169
|
+
task="test task",
|
170
|
+
validate=True,
|
171
|
+
)
|
172
|
+
|
173
|
+
# Verify validation was attempted and failed message printed
|
174
|
+
mock_parser.validate.assert_called_once()
|
175
|
+
# Check that error message was printed
|
176
|
+
print_calls = [str(call) for call in mock_print.call_args_list]
|
177
|
+
assert any("✗ Generated code has syntax errors" in str(call) for call in print_calls)
|
178
|
+
assert result is not None
|
179
|
+
|
180
|
+
@patch("machine_dialect.cfg.generate_with_ai.ConfigLoader")
|
181
|
+
@patch("machine_dialect.cfg.generate_with_ai.CFGParser")
|
182
|
+
@patch("builtins.print")
|
183
|
+
def test_generate_code_temperature_and_tokens(
|
184
|
+
self, mock_print: Mock, mock_parser_class: Mock, mock_loader_class: Mock
|
185
|
+
) -> None:
|
186
|
+
"""Test that temperature and max_tokens parameters are used."""
|
187
|
+
# Setup mocks
|
188
|
+
mock_loader = mock_loader_class.return_value
|
189
|
+
mock_config = MagicMock()
|
190
|
+
mock_config.key = "test-api-key"
|
191
|
+
mock_config.model = "gpt-4"
|
192
|
+
mock_loader.load.return_value = mock_config
|
193
|
+
|
194
|
+
mock_parser = mock_parser_class.return_value
|
195
|
+
mock_parser.validate.return_value = True
|
196
|
+
|
197
|
+
# Call with custom temperature and tokens
|
198
|
+
result = generate_code(
|
199
|
+
task="complex task",
|
200
|
+
temperature=0.2,
|
201
|
+
max_tokens=1000,
|
202
|
+
validate=True,
|
203
|
+
)
|
204
|
+
|
205
|
+
# Verify parameters were printed
|
206
|
+
print_calls = [str(call) for call in mock_print.call_args_list]
|
207
|
+
assert any("Temperature: 0.2" in str(call) for call in print_calls)
|
208
|
+
assert any("Max tokens: 1000" in str(call) for call in print_calls)
|
209
|
+
assert result is not None
|
210
|
+
|
211
|
+
|
212
|
+
class TestMain:
|
213
|
+
"""Test the main function."""
|
214
|
+
|
215
|
+
@patch("sys.argv", ["prog", "calculate area"])
|
216
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
217
|
+
def test_main_basic_task(self, mock_generate: Mock) -> None:
|
218
|
+
"""Test main with basic task argument."""
|
219
|
+
mock_generate.return_value = "Generated code"
|
220
|
+
|
221
|
+
result = main()
|
222
|
+
|
223
|
+
assert result == 0
|
224
|
+
mock_generate.assert_called_once_with(
|
225
|
+
task="calculate area",
|
226
|
+
api_key=None,
|
227
|
+
model=None,
|
228
|
+
temperature=0.7,
|
229
|
+
max_tokens=500,
|
230
|
+
validate=True,
|
231
|
+
)
|
232
|
+
|
233
|
+
@patch("sys.argv", ["prog", "test task", "--api-key", "my-key", "--model", "gpt-4"])
|
234
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
235
|
+
def test_main_with_overrides(self, mock_generate: Mock) -> None:
|
236
|
+
"""Test main with API key and model overrides."""
|
237
|
+
mock_generate.return_value = "Generated code"
|
238
|
+
|
239
|
+
result = main()
|
240
|
+
|
241
|
+
assert result == 0
|
242
|
+
mock_generate.assert_called_once_with(
|
243
|
+
task="test task",
|
244
|
+
api_key="my-key",
|
245
|
+
model="gpt-4",
|
246
|
+
temperature=0.7,
|
247
|
+
max_tokens=500,
|
248
|
+
validate=True,
|
249
|
+
)
|
250
|
+
|
251
|
+
@patch("sys.argv", ["prog", "test task", "--temperature", "0.3", "--max-tokens", "1000"])
|
252
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
253
|
+
def test_main_with_generation_params(self, mock_generate: Mock) -> None:
|
254
|
+
"""Test main with temperature and max-tokens parameters."""
|
255
|
+
mock_generate.return_value = "Generated code"
|
256
|
+
|
257
|
+
result = main()
|
258
|
+
|
259
|
+
assert result == 0
|
260
|
+
mock_generate.assert_called_once_with(
|
261
|
+
task="test task",
|
262
|
+
api_key=None,
|
263
|
+
model=None,
|
264
|
+
temperature=0.3,
|
265
|
+
max_tokens=1000,
|
266
|
+
validate=True,
|
267
|
+
)
|
268
|
+
|
269
|
+
@patch("sys.argv", ["prog", "test task", "--no-validate"])
|
270
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
271
|
+
def test_main_without_validation(self, mock_generate: Mock) -> None:
|
272
|
+
"""Test main with --no-validate flag."""
|
273
|
+
mock_generate.return_value = "Generated code"
|
274
|
+
|
275
|
+
result = main()
|
276
|
+
|
277
|
+
assert result == 0
|
278
|
+
mock_generate.assert_called_once_with(
|
279
|
+
task="test task",
|
280
|
+
api_key=None,
|
281
|
+
model=None,
|
282
|
+
temperature=0.7,
|
283
|
+
max_tokens=500,
|
284
|
+
validate=False,
|
285
|
+
)
|
286
|
+
|
287
|
+
@patch("sys.argv", ["prog", "test task", "--save", "output.md"])
|
288
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
289
|
+
@patch("builtins.open", new_callable=mock_open)
|
290
|
+
@patch("builtins.print")
|
291
|
+
def test_main_with_save_file(self, mock_print: Mock, mock_file: Mock, mock_generate: Mock) -> None:
|
292
|
+
"""Test main with --save option to write to file."""
|
293
|
+
mock_generate.return_value = "Generated code content"
|
294
|
+
|
295
|
+
result = main()
|
296
|
+
|
297
|
+
assert result == 0
|
298
|
+
mock_generate.assert_called_once()
|
299
|
+
|
300
|
+
# Verify file was written
|
301
|
+
mock_file.assert_called_once_with("output.md", "w")
|
302
|
+
mock_file().write.assert_called_once_with("Generated code content")
|
303
|
+
|
304
|
+
# Verify success message was printed
|
305
|
+
print_calls = [str(call) for call in mock_print.call_args_list]
|
306
|
+
assert any("Code saved to: output.md" in str(call) for call in print_calls)
|
307
|
+
|
308
|
+
@patch("sys.argv", ["prog", "test task"])
|
309
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
310
|
+
@patch("builtins.print")
|
311
|
+
def test_main_with_exception(self, mock_print: Mock, mock_generate: Mock) -> None:
|
312
|
+
"""Test main when generate_code raises an exception."""
|
313
|
+
mock_generate.side_effect = ValueError("API key not configured")
|
314
|
+
|
315
|
+
result = main()
|
316
|
+
|
317
|
+
assert result == 1
|
318
|
+
mock_generate.assert_called_once()
|
319
|
+
|
320
|
+
# Verify error message was printed
|
321
|
+
print_calls = [str(call) for call in mock_print.call_args_list]
|
322
|
+
assert any("Error: API key not configured" in str(call) for call in print_calls)
|
323
|
+
|
324
|
+
@patch("sys.argv", ["prog", "complex task", "--save", "/invalid/path/file.md"])
|
325
|
+
@patch("machine_dialect.cfg.generate_with_ai.generate_code")
|
326
|
+
@patch("builtins.open", side_effect=OSError("Permission denied"))
|
327
|
+
@patch("builtins.print")
|
328
|
+
def test_main_with_save_error(self, mock_print: Mock, mock_file: Mock, mock_generate: Mock) -> None:
|
329
|
+
"""Test main when saving to file fails."""
|
330
|
+
mock_generate.return_value = "Generated code"
|
331
|
+
|
332
|
+
result = main()
|
333
|
+
|
334
|
+
assert result == 1
|
335
|
+
mock_generate.assert_called_once()
|
336
|
+
|
337
|
+
# Verify error message was printed
|
338
|
+
print_calls = [str(call) for call in mock_print.call_args_list]
|
339
|
+
assert any("Error: Permission denied" in str(call) for call in print_calls)
|
340
|
+
|
341
|
+
def test_main_as_script(self) -> None:
|
342
|
+
"""Test that main can be called as a script."""
|
343
|
+
with patch("sys.argv", ["prog", "test"]):
|
344
|
+
with patch("machine_dialect.cfg.generate_with_ai.generate_code") as mock_gen:
|
345
|
+
mock_gen.return_value = "code"
|
346
|
+
# Import and run the module as __main__
|
347
|
+
import machine_dialect.cfg.generate_with_ai as module
|
348
|
+
|
349
|
+
# Simulate running as script
|
350
|
+
with patch.object(module, "__name__", "__main__"):
|
351
|
+
# This would normally trigger the if __name__ == "__main__" block
|
352
|
+
# but we'll call main directly for testing
|
353
|
+
exit_code = module.main()
|
354
|
+
assert exit_code == 0
|
@@ -0,0 +1,256 @@
|
|
1
|
+
"""Tests for the grammar-based OpenAI generation module."""
|
2
|
+
|
3
|
+
from unittest.mock import MagicMock
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
from machine_dialect.cfg.openai_generation import _get_machine_dialect_cfg, generate_with_openai, validate_model_support
|
8
|
+
|
9
|
+
|
10
|
+
class TestGenerateWithOpenAI:
|
11
|
+
"""Test the grammar-based generate_with_openai function."""
|
12
|
+
|
13
|
+
def test_gpt5_cfg_generation(self) -> None:
|
14
|
+
"""Test generation with GPT-5 using context-free grammar."""
|
15
|
+
# Mock OpenAI client
|
16
|
+
mock_client = MagicMock()
|
17
|
+
mock_response = MagicMock()
|
18
|
+
|
19
|
+
# Set up the response to have output_text directly (primary path)
|
20
|
+
mock_response.output_text = "Set x to _10_.\nGive back x."
|
21
|
+
# Also set up output as fallback
|
22
|
+
mock_output = MagicMock()
|
23
|
+
mock_output.input = "Set x to _10_.\nGive back x."
|
24
|
+
mock_response.output = [MagicMock(), mock_output] # First is text, second is tool output
|
25
|
+
|
26
|
+
mock_client.responses.create.return_value = mock_response
|
27
|
+
|
28
|
+
result = generate_with_openai(
|
29
|
+
client=mock_client,
|
30
|
+
model="gpt-5",
|
31
|
+
task_description="set x to 10 and display it",
|
32
|
+
max_tokens=200,
|
33
|
+
temperature=0.7,
|
34
|
+
)
|
35
|
+
|
36
|
+
# Result should be a tuple of (code, token_info)
|
37
|
+
assert isinstance(result, tuple)
|
38
|
+
assert len(result) == 2
|
39
|
+
code, token_info = result
|
40
|
+
assert code == "Set x to _10_.\nGive back x."
|
41
|
+
assert isinstance(token_info, dict)
|
42
|
+
|
43
|
+
# Verify API call structure
|
44
|
+
call_args = mock_client.responses.create.call_args
|
45
|
+
assert call_args.kwargs["model"] == "gpt-5"
|
46
|
+
# Note: GPT-5 doesn't support max_completion_tokens or temperature
|
47
|
+
assert "max_completion_tokens" not in call_args.kwargs
|
48
|
+
assert "temperature" not in call_args.kwargs
|
49
|
+
assert call_args.kwargs["parallel_tool_calls"] is False
|
50
|
+
|
51
|
+
# Check that custom tool with CFG was provided
|
52
|
+
tools = call_args.kwargs["tools"]
|
53
|
+
assert len(tools) == 1
|
54
|
+
assert tools[0]["type"] == "custom"
|
55
|
+
assert tools[0]["name"] == "machine_dialect_generator"
|
56
|
+
assert "format" in tools[0]
|
57
|
+
|
58
|
+
# Check CFG format - now using Lark syntax
|
59
|
+
cfg = tools[0]["format"]
|
60
|
+
assert cfg["type"] == "grammar"
|
61
|
+
assert cfg["syntax"] == "lark"
|
62
|
+
assert "definition" in cfg
|
63
|
+
# The definition is now a Lark grammar string
|
64
|
+
assert isinstance(cfg["definition"], str)
|
65
|
+
assert "start:" in cfg["definition"]
|
66
|
+
assert "statement:" in cfg["definition"]
|
67
|
+
|
68
|
+
def test_non_gpt5_model_raises_error(self) -> None:
|
69
|
+
"""Test that non-GPT-5 models raise an error."""
|
70
|
+
mock_client = MagicMock()
|
71
|
+
|
72
|
+
with pytest.raises(ValueError, match="does not support context-free grammar"):
|
73
|
+
generate_with_openai(client=mock_client, model="gpt-4o", task_description="test task", max_tokens=100)
|
74
|
+
|
75
|
+
# Should not have made any API calls
|
76
|
+
mock_client.responses.create.assert_not_called()
|
77
|
+
|
78
|
+
def test_gpt5_mini_supported(self) -> None:
|
79
|
+
"""Test that gpt-5-mini is recognized as supporting CFG."""
|
80
|
+
mock_client = MagicMock()
|
81
|
+
mock_response = MagicMock()
|
82
|
+
# Set up the response to have output_text directly
|
83
|
+
mock_response.output_text = 'Give back _"Hello"_.'
|
84
|
+
# Also set up output as fallback
|
85
|
+
mock_output = MagicMock()
|
86
|
+
mock_output.input = 'Give back _"Hello"_.'
|
87
|
+
mock_response.output = [MagicMock(), mock_output]
|
88
|
+
mock_client.responses.create.return_value = mock_response
|
89
|
+
|
90
|
+
result = generate_with_openai(
|
91
|
+
client=mock_client, model="gpt-5-mini", task_description="say hello", max_tokens=50
|
92
|
+
)
|
93
|
+
|
94
|
+
# Result should be a tuple of (code, token_info)
|
95
|
+
assert isinstance(result, tuple)
|
96
|
+
assert len(result) == 2
|
97
|
+
code, token_info = result
|
98
|
+
assert code == 'Give back _"Hello"_.'
|
99
|
+
assert isinstance(token_info, dict)
|
100
|
+
assert mock_client.responses.create.called
|
101
|
+
|
102
|
+
def test_empty_response_raises_error(self) -> None:
|
103
|
+
"""Test that empty response raises ValueError."""
|
104
|
+
mock_client = MagicMock()
|
105
|
+
mock_response = MagicMock()
|
106
|
+
# Make response have no valid attributes
|
107
|
+
mock_response.output_text = None
|
108
|
+
mock_response.output = [] # Empty output
|
109
|
+
mock_client.responses.create.return_value = mock_response
|
110
|
+
|
111
|
+
with pytest.raises(ValueError, match="Failed to extract valid code"):
|
112
|
+
generate_with_openai(client=mock_client, model="gpt-5", task_description="test task", max_tokens=100)
|
113
|
+
|
114
|
+
def test_empty_code_raises_error(self) -> None:
|
115
|
+
"""Test that empty generated code raises ValueError."""
|
116
|
+
mock_client = MagicMock()
|
117
|
+
mock_response = MagicMock()
|
118
|
+
# Set up response to have empty code
|
119
|
+
mock_response.output_text = "" # Empty code
|
120
|
+
del mock_response.output # Remove output attribute
|
121
|
+
mock_client.responses.create.return_value = mock_response
|
122
|
+
|
123
|
+
with pytest.raises(ValueError, match="Failed to extract valid code"):
|
124
|
+
generate_with_openai(client=mock_client, model="gpt-5", task_description="test task", max_tokens=100)
|
125
|
+
|
126
|
+
def test_input_messages_structure(self) -> None:
|
127
|
+
"""Test that input messages are structured correctly."""
|
128
|
+
mock_client = MagicMock()
|
129
|
+
mock_response = MagicMock()
|
130
|
+
# Set up the response to have output_text directly
|
131
|
+
mock_response.output_text = "test"
|
132
|
+
# Also set up output as fallback
|
133
|
+
mock_output = MagicMock()
|
134
|
+
mock_output.input = "test"
|
135
|
+
mock_response.output = [MagicMock(), mock_output]
|
136
|
+
mock_client.responses.create.return_value = mock_response
|
137
|
+
|
138
|
+
result = generate_with_openai(
|
139
|
+
client=mock_client, model="gpt-5-nano", task_description="test task", max_tokens=50
|
140
|
+
)
|
141
|
+
assert isinstance(result, tuple) # Verify it returns a tuple
|
142
|
+
|
143
|
+
# Get the input messages passed to the API
|
144
|
+
call_args = mock_client.responses.create.call_args
|
145
|
+
messages = call_args.kwargs["input"]
|
146
|
+
|
147
|
+
assert len(messages) == 2
|
148
|
+
|
149
|
+
# Developer message
|
150
|
+
assert messages[0]["role"] == "developer"
|
151
|
+
assert "Machine Dialect™ code generator" in messages[0]["content"]
|
152
|
+
assert "context-free grammar" in messages[0]["content"]
|
153
|
+
|
154
|
+
# User message
|
155
|
+
assert messages[1]["role"] == "user"
|
156
|
+
assert "test task" in messages[1]["content"]
|
157
|
+
|
158
|
+
|
159
|
+
class TestValidateModelSupport:
|
160
|
+
"""Test the validate_model_support function."""
|
161
|
+
|
162
|
+
def test_gpt5_models_supported(self) -> None:
|
163
|
+
"""Test that GPT-5 models are recognized as supported."""
|
164
|
+
assert validate_model_support("gpt-5") is True
|
165
|
+
assert validate_model_support("GPT-5") is True
|
166
|
+
assert validate_model_support("gpt-5-mini") is True
|
167
|
+
assert validate_model_support("GPT-5-MINI") is True
|
168
|
+
assert validate_model_support("gpt-5-nano") is True
|
169
|
+
assert validate_model_support("gpt-5-Nano") is True
|
170
|
+
|
171
|
+
def test_non_gpt5_models_not_supported(self) -> None:
|
172
|
+
"""Test that non-GPT-5 models are not supported."""
|
173
|
+
assert validate_model_support("gpt-4") is False
|
174
|
+
assert validate_model_support("gpt-4o") is False
|
175
|
+
assert validate_model_support("gpt-3.5-turbo") is False
|
176
|
+
assert validate_model_support("claude-3") is False
|
177
|
+
assert validate_model_support("gemini-pro") is False
|
178
|
+
|
179
|
+
def test_partial_matches(self) -> None:
|
180
|
+
"""Test models with GPT-5 in the name are supported."""
|
181
|
+
assert validate_model_support("gpt-5-2025-08-07") is True
|
182
|
+
assert validate_model_support("gpt-5-mini-latest") is True
|
183
|
+
assert validate_model_support("custom-gpt-5-model") is True
|
184
|
+
|
185
|
+
|
186
|
+
class TestMachineDialectCFG:
|
187
|
+
"""Test the Machine Dialect™ CFG structure."""
|
188
|
+
|
189
|
+
def test_cfg_structure(self) -> None:
|
190
|
+
"""Test that the CFG has the correct structure."""
|
191
|
+
cfg = _get_machine_dialect_cfg()
|
192
|
+
|
193
|
+
# Check top-level structure
|
194
|
+
assert cfg["type"] == "grammar"
|
195
|
+
assert cfg["syntax"] == "lark" # Now using Lark syntax
|
196
|
+
assert "definition" in cfg
|
197
|
+
|
198
|
+
# The definition is now a Lark grammar string
|
199
|
+
definition = cfg["definition"]
|
200
|
+
assert isinstance(definition, str)
|
201
|
+
|
202
|
+
# Check that key rules exist in the Lark grammar
|
203
|
+
assert "start:" in definition or "program:" in definition
|
204
|
+
assert "statement:" in definition
|
205
|
+
assert "set_stmt:" in definition
|
206
|
+
assert "give_back_stmt:" in definition
|
207
|
+
assert "if_stmt:" in definition
|
208
|
+
assert "expression:" in definition
|
209
|
+
|
210
|
+
# Check that terminals are defined (using new literal patterns)
|
211
|
+
assert "LITERAL_" in definition or "IDENT" in definition
|
212
|
+
assert "IDENTIFIER" in definition
|
213
|
+
|
214
|
+
def test_lark_grammar_content(self) -> None:
|
215
|
+
"""Test that the Lark grammar has expected content."""
|
216
|
+
cfg = _get_machine_dialect_cfg()
|
217
|
+
grammar = cfg["definition"]
|
218
|
+
|
219
|
+
# Check for statement rules
|
220
|
+
assert "program:" in grammar or "start:" in grammar
|
221
|
+
assert "statement:" in grammar
|
222
|
+
assert "set_stmt" in grammar
|
223
|
+
assert "give_back_stmt" in grammar
|
224
|
+
|
225
|
+
# Check for set and give back statements
|
226
|
+
assert 'set_stmt: "Set"i identifier "to"i expression' in grammar
|
227
|
+
assert 'give_back_stmt: ("Give"i "back"i | "Gives"i "back"i) expression' in grammar
|
228
|
+
|
229
|
+
# Check for expression rules
|
230
|
+
assert "expression:" in grammar or "expr:" in grammar
|
231
|
+
assert "or" in grammar.lower()
|
232
|
+
assert "and" in grammar.lower()
|
233
|
+
|
234
|
+
# Check for comparison operators
|
235
|
+
assert '"<"' in grammar
|
236
|
+
assert '">"' in grammar
|
237
|
+
assert '"equals"i' in grammar or "equals" in grammar.lower()
|
238
|
+
|
239
|
+
# Check for arithmetic operators
|
240
|
+
assert '"+"' in grammar
|
241
|
+
assert '"-"' in grammar
|
242
|
+
assert '"*"' in grammar
|
243
|
+
assert '"/"' in grammar
|
244
|
+
|
245
|
+
def test_grammar_terminals(self) -> None:
|
246
|
+
"""Test that terminals are properly defined in Lark grammar."""
|
247
|
+
cfg = _get_machine_dialect_cfg()
|
248
|
+
grammar = cfg["definition"]
|
249
|
+
|
250
|
+
# Check terminal definitions (new pattern with literals)
|
251
|
+
assert "IDENTIFIER" in grammar or "IDENT" in grammar
|
252
|
+
assert "LITERAL_" in grammar # Check for literal patterns
|
253
|
+
|
254
|
+
# Check whitespace handling
|
255
|
+
assert "%import common.WS" in grammar
|
256
|
+
assert "%ignore WS" in grammar
|