mxrb 0.1.0

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.
Files changed (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "json"
5
+
6
+ module Mxrb
7
+ module Functional
8
+ Hook = Data.define(:target, :arguments)
9
+ CountExpectation = Data.define(:entity, :xpath, :equals)
10
+ TestCase = Data.define(
11
+ :name, :target, :arguments, :timeout,
12
+ :expected_return, :counts, :setup, :cleanup
13
+ ) do
14
+ class << self
15
+ alias data_new new
16
+
17
+ def new(name, target, arguments, timeout, expected_return = nil,
18
+ counts = [].freeze, setup = nil, cleanup = nil)
19
+ data_new(
20
+ name, target, arguments, timeout,
21
+ expected_return, counts, setup, cleanup
22
+ )
23
+ end
24
+ end
25
+ end
26
+ Definition = Data.define(:tests) do
27
+ def empty? = tests.empty?
28
+ end
29
+ TestResult = Data.define(:name, :passed, :message) do
30
+ def passed? = passed
31
+ def failed? = !passed
32
+ end
33
+ Result = Data.define(:tests, :finished) do
34
+ def passed? = finished && tests.all?(&:passed?)
35
+ def finished? = finished
36
+ def failures = tests.select(&:failed?)
37
+ end
38
+
39
+ # Ruby-only definition of runtime microflow tests.
40
+ class Suite
41
+ attr_reader :tests
42
+
43
+ def initialize
44
+ @tests = []
45
+ end
46
+
47
+ def microflow(name, call:, pass: {}, timeout: 60, expect: {},
48
+ before: nil, after: nil)
49
+ label = name.to_s.strip
50
+ target = call.to_s.strip
51
+ raise ArgumentError, "functional test name cannot be empty" if label.empty?
52
+ unless target.match?(/\A[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\z/)
53
+ raise ArgumentError, "microflow call must be a qualified Mendix name"
54
+ end
55
+ raise ArgumentError, "functional test timeout must be positive" unless timeout.to_f.positive?
56
+
57
+ arguments = pass.to_h.transform_keys(&:to_s).transform_values(&:to_s).freeze
58
+ expectations = expect.to_h
59
+ expected_return = expectations[:return] || expectations["return"]
60
+ counts = normalize_counts(expectations[:count] || expectations["count"])
61
+ @tests << TestCase.new(
62
+ label, target, arguments, timeout.to_f, expected_return&.to_s,
63
+ counts, normalize_hook(before), normalize_hook(after)
64
+ )
65
+ self
66
+ end
67
+
68
+ def evaluate(path)
69
+ instance_eval(File.read(path), path, 1)
70
+ self
71
+ end
72
+
73
+ def definition
74
+ Definition.new(@tests.dup.freeze)
75
+ end
76
+
77
+ private
78
+
79
+ def normalize_hook(value)
80
+ return unless value
81
+
82
+ options = value.is_a?(Hash) ? value : { call: value }
83
+ target = (options[:call] || options["call"]).to_s
84
+ unless target.match?(/\A[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\z/)
85
+ raise ArgumentError, "functional hook must call a qualified microflow"
86
+ end
87
+ arguments = (options[:pass] || options["pass"] || {}).to_h
88
+ .transform_keys(&:to_s).transform_values(&:to_s).freeze
89
+ Hook.new(target, arguments)
90
+ end
91
+
92
+ def normalize_counts(value)
93
+ values = value.is_a?(Array) ? value : [value]
94
+ values.compact.map do |item|
95
+ options = item.to_h
96
+ entity = (options[:entity] || options["entity"]).to_s
97
+ equals = options.key?(:equals) ? options[:equals] : options["equals"]
98
+ unless entity.match?(/\A[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\z/)
99
+ raise ArgumentError, "count expectation requires a qualified entity"
100
+ end
101
+ raise ArgumentError, "count expectation requires a non-negative integer" unless
102
+ equals.is_a?(Integer) && equals >= 0
103
+
104
+ CountExpectation.new(
105
+ entity, (options[:xpath] || options["xpath"])&.to_s, equals
106
+ )
107
+ end.freeze
108
+ end
109
+ end
110
+
111
+ # Adds an isolated after-startup runner to a writable disposable MPR copy.
112
+ class Instrumenter
113
+ MODULE_NAME = "MxrbTests"
114
+ RUNNER_NAME = "RunAll"
115
+ LOG_NODE = "'MXRB_TEST'".freeze
116
+
117
+ def initialize(path, definition)
118
+ @path = File.expand_path(path)
119
+ @definition = definition
120
+ end
121
+
122
+ def instrument!
123
+ raise FunctionalTestError, "functional test suite is empty" if @definition.empty?
124
+
125
+ version = validate_targets!
126
+ mod = build_module
127
+ Writer.new(
128
+ @path,
129
+ version: version, modules: [mod.to_h],
130
+ security: nil, native_units_path: nil
131
+ ).write!
132
+ select_after_startup!
133
+ self
134
+ end
135
+
136
+ def runner = "#{MODULE_NAME}.#{RUNNER_NAME}"
137
+
138
+ private
139
+
140
+ def validate_targets!
141
+ Mxrb.open(@path) do |project|
142
+ @definition.tests.each do |test|
143
+ validate_call!(project, test.target, test.arguments)
144
+ validate_call!(project, test.setup.target, test.setup.arguments) if test.setup
145
+ validate_call!(project, test.cleanup.target, test.cleanup.arguments) if test.cleanup
146
+ test.counts.each do |expectation|
147
+ unless project.find_artifact(expectation.entity, kind: :entity)
148
+ raise FunctionalTestError, "entity #{expectation.entity} not found"
149
+ end
150
+ end
151
+ end
152
+ project.mendix_version
153
+ end
154
+ end
155
+
156
+ def validate_call!(project, target, arguments)
157
+ artifact = project.find_artifact(target, kind: :microflow)
158
+ raise FunctionalTestError, "microflow #{target} not found" unless artifact
159
+
160
+ doc = project.parse_bson(project.raw_unit(artifact.unit_id))
161
+ collection = doc["MicroflowParameterCollection"] || doc["Parameters"] || {}
162
+ parameter_docs = collection.is_a?(Hash) ?
163
+ IO::BsonCodec.parse_array(collection["Parameters"])[:items] : []
164
+ parameter_docs += IO::BsonCodec.parse_array(
165
+ doc.dig("ObjectCollection", "Objects")
166
+ )[:items].select { _1["$Type"] == "Microflows$MicroflowParameter" }
167
+ parameters = parameter_docs.filter_map do |parameter|
168
+ parameter["Name"] || parameter["name"] if parameter.is_a?(Hash)
169
+ end
170
+ unknown = arguments.keys - parameters
171
+ missing = parameters - arguments.keys
172
+ return if unknown.empty? && missing.empty?
173
+
174
+ raise FunctionalTestError,
175
+ "microflow #{target} arguments mismatch " \
176
+ "(missing: #{missing.join(', ')}, unknown: #{unknown.join(', ')})"
177
+ end
178
+
179
+ def build_module
180
+ mod = Dsl::ModuleBuilder.new(MODULE_NAME)
181
+ tests = @definition.tests
182
+ wrapper_names = tests.each_index.map { wrapper_name(_1) }
183
+ emit_hook = lambda do |flow, hook|
184
+ flow.call_microflow(hook.target, pass: hook.arguments) if hook
185
+ end
186
+ tests.each_with_index do |test, index|
187
+ mod.microflow(wrapper_name(index), kind: :test) do
188
+ return_type :Boolean
189
+ emit_hook.call(self, test.setup)
190
+ call_microflow(
191
+ test.target, pass: test.arguments,
192
+ as: (test.expected_return ? :mxrb_actual : nil)
193
+ )
194
+ conditions = []
195
+ conditions << "$mxrb_actual = #{test.expected_return}" if test.expected_return
196
+ test.counts.each_with_index do |expectation, count_index|
197
+ list = :"mxrb_items_#{count_index + 1}"
198
+ count = :"mxrb_count_#{count_index + 1}"
199
+ retrieve_objects expectation.entity, as: list, xpath: expectation.xpath
200
+ aggregate list, function: :count, as: count
201
+ conditions << "$#{count} = #{expectation.equals}"
202
+ end
203
+ if conditions.empty?
204
+ emit_hook.call(self, test.cleanup)
205
+ return_value "true"
206
+ else
207
+ emit_hook.call(self, test.cleanup)
208
+ return_value "(#{conditions.join(' and ')})"
209
+ end
210
+ rescue_all do
211
+ emit_hook.call(self, test.cleanup)
212
+ return_value "false"
213
+ end
214
+ end
215
+ end
216
+ mod.microflow(RUNNER_NAME, kind: :test) do
217
+ return_type :Boolean
218
+ tests.each_with_index do |test, index|
219
+ variable = :"passed_#{index + 1}"
220
+ call_microflow "#{MODULE_NAME}.#{wrapper_names.fetch(index)}", as: variable
221
+ decision "$#{variable}" do
222
+ on(true) do
223
+ log_message "[MXRB_TEST] PASS #{test.name}", node: LOG_NODE
224
+ end
225
+ on(false) do
226
+ log_message "[MXRB_TEST] FAIL #{test.name}", level: :error, node: LOG_NODE
227
+ end
228
+ end
229
+ end
230
+ log_message "[MXRB_TEST] DONE", node: LOG_NODE
231
+ return_value "true"
232
+ end
233
+ mod
234
+ end
235
+
236
+ def wrapper_name(index) = format("Test_%03d", index + 1)
237
+
238
+ def select_after_startup!
239
+ mpr = IO::MprFile.open(@path)
240
+ raw = mpr.all_units.find { mpr.parse_contents(_1)["$Type"] == "Settings$ProjectSettings" }
241
+ raise FunctionalTestError, "project settings unit not found" unless raw
242
+
243
+ doc = mpr.parse_contents(raw)
244
+ settings = IO::BsonCodec.parse_array(doc["Settings"])[:items]
245
+ model_settings = settings.find { _1["$Type"] == "Settings$ModelSettings" }
246
+ raise FunctionalTestError, "model settings part not found" unless model_settings
247
+
248
+ model_settings["AfterStartupMicroflow"] = runner
249
+ mpr.update_unit(raw.fetch("UnitID"), doc)
250
+ ensure
251
+ mpr&.close
252
+ end
253
+ end
254
+
255
+ class LogParser
256
+ PATTERN = /\[MXRB_TEST\] (PASS|FAIL) (.+)$/
257
+
258
+ def parse(text)
259
+ tests = text.each_line.filter_map do |line|
260
+ match = line.strip.match(PATTERN)
261
+ next unless match
262
+
263
+ passed = match[1] == "PASS"
264
+ TestResult.new(match[2], passed, passed ? "passed" : "microflow failed")
265
+ end
266
+ Result.new(tests.freeze, text.include?("[MXRB_TEST] DONE"))
267
+ end
268
+ end
269
+
270
+ class Reporter
271
+ def write_json(execution, path)
272
+ payload = {
273
+ passed: execution.passed?,
274
+ finished: execution.result.finished?,
275
+ elapsed: execution.elapsed,
276
+ tests: execution.result.tests.map do |test|
277
+ { name: test.name, passed: test.passed?, message: test.message }
278
+ end
279
+ }
280
+ File.write(path, JSON.pretty_generate(payload) << "\n")
281
+ path
282
+ end
283
+
284
+ def write_junit(execution, path)
285
+ tests = execution.result.tests
286
+ failures = tests.count(&:failed?)
287
+ cases = tests.map do |test|
288
+ name = CGI.escapeHTML(test.name)
289
+ if test.passed?
290
+ %( <testcase name="#{name}"/>)
291
+ else
292
+ message = CGI.escapeHTML(test.message)
293
+ %( <testcase name="#{name}"><failure message="#{message}"/></testcase>)
294
+ end
295
+ end.join("\n")
296
+ xml = <<~XML
297
+ <?xml version="1.0" encoding="UTF-8"?>
298
+ <testsuite name="mxrb" tests="#{tests.size}" failures="#{failures}" time="#{execution.elapsed}">
299
+ #{cases}
300
+ </testsuite>
301
+ XML
302
+ File.write(path, xml)
303
+ path
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Mxrb
7
+ module Github
8
+ # Maps a Compare::Result to GitHub annotations.
9
+ #
10
+ # Supports two output modes:
11
+ # 1. GitHub Actions workflow commands (stdout lines) for CI annotation
12
+ # 2. `gh pr comment` for pull request review comments
13
+ #
14
+ # Usage:
15
+ # result = Mxrb::Compare::Comparator.new(base_mpr, head_mpr).compare
16
+ # ann = Mxrb::Github::Annotator.new(result, exported_dir: "out/", repo: "org/repo")
17
+ # ann.print_actions_annotations # write to stdout for GH Actions
18
+ # ann.post_pr_comment!(pr_number: 42) # post summary via gh CLI
19
+ class Annotator
20
+ # Maps Compare path segments to file-layer hints used in exported trees.
21
+ LAYER_MAP = {
22
+ "entities" => "domain",
23
+ "attributes" => "domain",
24
+ "microflows" => "logic",
25
+ "nanoflows" => "presentation",
26
+ "pages" => "presentation",
27
+ "menus" => "presentation",
28
+ }.freeze
29
+
30
+ Annotation = Data.define(:level, :title, :message, :file, :line)
31
+
32
+ def initialize(compare_result, exported_dir: nil, repo: nil)
33
+ @result = compare_result
34
+ @exported_dir = exported_dir && File.expand_path(exported_dir)
35
+ @repo = repo
36
+ end
37
+
38
+ # Returns an array of Annotation structs for each changed artifact.
39
+ def annotations
40
+ @annotations ||= @result.changes.map do |change|
41
+ build_annotation(change)
42
+ end
43
+ end
44
+
45
+ # Emits GitHub Actions workflow command lines to stdout.
46
+ # When run inside a GH Actions job these create inline PR annotations.
47
+ def print_actions_annotations(out: $stdout)
48
+ annotations.each do |ann|
49
+ file_part = ann.file ? "file=#{ann.file}" : ""
50
+ line_part = ann.line ? ",line=#{ann.line}" : ""
51
+ out.puts "::#{ann.level} #{file_part}#{line_part},title=#{ann.title}::#{ann.message}"
52
+ end
53
+ end
54
+
55
+ # Posts a Markdown summary comment to a GitHub PR via the `gh` CLI.
56
+ def post_pr_comment!(pr_number:, repo: @repo)
57
+ raise ArgumentError, "repo required (org/name)" unless repo
58
+ raise ArgumentError, "pr_number required" unless pr_number
59
+
60
+ body = build_comment_body
61
+ cmd = ["gh", "pr", "comment", pr_number.to_s, "--repo", repo, "--body", body]
62
+ out, err, status = Open3.capture3(*cmd)
63
+ raise "gh pr comment failed: #{err}" unless status.success?
64
+
65
+ out.strip
66
+ end
67
+
68
+ # Returns the comment body Markdown string without posting it.
69
+ def comment_body
70
+ build_comment_body
71
+ end
72
+
73
+ private
74
+
75
+ def build_annotation(change)
76
+ op = change.operation
77
+ path = change.path
78
+ label = path.last.to_s
79
+ module_name = infer_module(change)
80
+
81
+ level = op == :removed ? "warning" : (op == :added ? "notice" : "notice")
82
+ title = "#{op.to_s.capitalize}: #{label}"
83
+ message = format_message(change)
84
+ file = exported_file(path, module_name)
85
+ line = find_line_in_file(file, label)
86
+
87
+ Annotation.new(level: level, title: title, message: message, file: file, line: line)
88
+ end
89
+
90
+ def format_message(change)
91
+ op = change.operation
92
+ path = change.path.join(" › ")
93
+ case op
94
+ when :added then "Added: #{path}"
95
+ when :removed then "Removed: #{path}"
96
+ when :changed
97
+ before = change.before.inspect
98
+ after = change.after.inspect
99
+ "Changed: #{path} (#{before} → #{after})"
100
+ end
101
+ end
102
+
103
+ def infer_module(change)
104
+ path = change.path
105
+ # Compare paths are [:modules, "ModuleName", :kind, "Name"]
106
+ return path[1].to_s if path.first.to_s == "modules" && path.size > 1
107
+
108
+ nil
109
+ end
110
+
111
+ def exported_file(path, module_name)
112
+ return nil unless @exported_dir && module_name
113
+
114
+ # Find the first path segment that maps to a layer
115
+ type_segment = path.find { LAYER_MAP.key?(_1.to_s) }
116
+ return nil unless type_segment
117
+
118
+ layer = LAYER_MAP[type_segment.to_s]
119
+ type_idx = path.index(type_segment)
120
+ artifact_name = path[type_idx + 1]&.to_s&.downcase || path.last.to_s.downcase
121
+
122
+ relative_path = {
123
+ "domain" => ["domain", "model.rb"],
124
+ "logic" => ["logic", "#{artifact_name}.rb"],
125
+ "presentation" => ["presentation", "#{artifact_name}.rb"]
126
+ }.fetch(layer)
127
+ File.join(@exported_dir, "modules", module_name, *relative_path)
128
+ end
129
+
130
+ def find_line_in_file(file, label)
131
+ return nil unless file && File.file?(file.to_s)
132
+
133
+ File.foreach(file).with_index(1) do |line, n|
134
+ return n if line.include?(label)
135
+ end
136
+ nil
137
+ end
138
+
139
+ def build_comment_body
140
+ lines = ["## MXRB Semantic Diff\n"]
141
+ lines << "| Operation | Artifact | Detail |"
142
+ lines << "|-----------|----------|--------|"
143
+
144
+ @result.changes.each do |change|
145
+ op = change.operation.to_s.capitalize
146
+ path = change.path.join(" › ")
147
+ detail = case change.operation
148
+ when :changed then "`#{change.before}` → `#{change.after}`"
149
+ else ""
150
+ end
151
+ icon = { added: "✅", removed: "❌", changed: "✏️" }[change.operation] || "ℹ️"
152
+ lines << "| #{icon} #{op} | `#{path}` | #{detail} |"
153
+ end
154
+
155
+ lines << ""
156
+ lines << "_Generated by [MXRB](https://github.com/lucamykael/mxrb)_"
157
+ lines.join("\n")
158
+ end
159
+ end
160
+ end
161
+ end