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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- 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
|