julewire-core 1.0.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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. metadata +237 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Contracts
7
+ module Runtime
8
+ def assert_julewire_runtime_integration_contract(**options)
9
+ run_julewire_runtime_contract(options)
10
+ end
11
+
12
+ def assert_julewire_execution_boundary_contract(**options)
13
+ run_julewire_execution_boundary_contract(options)
14
+ end
15
+
16
+ def assert_julewire_failure_containment_contract(configure:, destination_name: :default, emit: nil)
17
+ Julewire.configure(&configure)
18
+
19
+ assert_nil((emit || default_failure_contract_emit).call)
20
+
21
+ health = Julewire.health
22
+ destination_health = health.dig(:pipeline, :destinations, destination_name.to_sym)
23
+
24
+ assert_equal :degraded, health.fetch(:status)
25
+ assert_kind_of Hash, destination_health
26
+
27
+ [health, destination_health]
28
+ end
29
+
30
+ def assert_julewire_record_source_contract(
31
+ records:,
32
+ event:,
33
+ source:,
34
+ **options
35
+ )
36
+ record = find_contract_record(records, options.fetch(:event_path, %w[event]), event)
37
+
38
+ assert_equal source.to_s, fetch_contract_path(record, options.fetch(:source_path, %w[source])).to_s
39
+ assert_contract_optional_source_field(record, options, :logger)
40
+ assert_contract_optional_source_field(record, options, :kind)
41
+
42
+ record
43
+ end
44
+
45
+ private
46
+
47
+ def assert_contract_optional_source_field(record, options, field)
48
+ expected = options[field]
49
+ return unless expected
50
+
51
+ path = options.fetch(:"#{field}_path", [field.to_s])
52
+ assert_equal expected.to_s, fetch_contract_path(record, path).to_s
53
+ end
54
+
55
+ def run_julewire_runtime_contract(options)
56
+ Julewire.configure(&options.fetch(:configure))
57
+ emit_runtime_contract_records
58
+
59
+ records = options.fetch(:records).call
60
+ point = find_contract_record(records, options.fetch(:event_path), point_event(options))
61
+ summary = find_contract_record(records, options.fetch(:event_path), summary_event(options))
62
+ health = Julewire.health
63
+
64
+ assert_runtime_contract_records(point, summary, health, options)
65
+ [point, summary, health]
66
+ end
67
+
68
+ def run_julewire_execution_boundary_contract(options)
69
+ Julewire.configure(&options.fetch(:configure))
70
+ options.fetch(:exercise).call(**execution_boundary_probe(options))
71
+ Julewire.flush
72
+
73
+ records = options.fetch(:records).call
74
+ point = find_contract_record(records, options.fetch(:event_path), point_event(options))
75
+ summary = find_contract_record(records, options.fetch(:event_path), summary_event(options))
76
+ health = Julewire.health
77
+
78
+ assert_runtime_contract_records(point, summary, health, options)
79
+ [point, summary, health]
80
+ end
81
+
82
+ def emit_runtime_contract_records
83
+ Julewire.with_execution(
84
+ type: :contract,
85
+ id: "contract-1",
86
+ summary_event: "contract.completed",
87
+ summary_source: "contract"
88
+ ) do
89
+ Julewire.context.add(request_id: "request-1")
90
+ Julewire.carry.add(http: { request_headers: { traceparent: contract_traceparent } })
91
+ Julewire.summary.add(total: 2)
92
+ Julewire.emit(event: point_event({}), source: "contract", message: "point", payload: { value: 1 })
93
+ end
94
+ Julewire.flush
95
+ end
96
+
97
+ def assert_runtime_contract_records(point, summary, health, options)
98
+ assert_equal "request-1", fetch_contract_path(point, options.fetch(:context_path) + [:request_id])
99
+ assert_equal 2, fetch_contract_path(summary, options.fetch(:summary_payload_path) + [:total])
100
+ assert_runtime_contract_carry(point, options[:carry_path]) if options[:carry_path]
101
+
102
+ assert health.dig(:pipeline, :configured)
103
+ assert_kind_of Hash, health.dig(:pipeline, :destinations, contract_destination_name(options))
104
+ end
105
+
106
+ def assert_runtime_contract_carry(point, carry_path)
107
+ path = carry_path + %i[http request_headers traceparent]
108
+
109
+ assert_equal contract_traceparent, fetch_contract_path(point, path)
110
+ end
111
+
112
+ def contract_destination_name(options)
113
+ options.fetch(:destination_name, :default).to_sym
114
+ end
115
+
116
+ def default_failure_contract_emit
117
+ lambda do
118
+ Julewire.emit(
119
+ event: "contract.failure",
120
+ source: "contract",
121
+ message: "failure probe",
122
+ payload: { token: "secret" }
123
+ )
124
+ end
125
+ end
126
+
127
+ def execution_boundary_probe(options)
128
+ {
129
+ add_summary: -> { Julewire.summary.add(total: 2) },
130
+ carry: { http: { request_headers: { traceparent: contract_traceparent } } },
131
+ context: { request_id: "request-1" },
132
+ emit_point: lambda do
133
+ Julewire.emit(event: point_event(options), source: "contract", message: "point", payload: { value: 1 })
134
+ end,
135
+ summary_event: summary_event(options),
136
+ traceparent: contract_traceparent
137
+ }
138
+ end
139
+
140
+ def point_event(options)
141
+ options.fetch(:point_event, "contract.point")
142
+ end
143
+
144
+ def summary_event(options)
145
+ options.fetch(:summary_event, "contract.completed")
146
+ end
147
+
148
+ def find_contract_record(records, event_path, event)
149
+ record = records.find { fetch_contract_path(it, event_path) == event }
150
+ return record if record
151
+
152
+ flunk("expected contract record #{event.inspect}, got #{records.inspect}")
153
+ end
154
+
155
+ def fetch_contract_path(value, path)
156
+ path.reduce(value) { |current, key| fetch_contract_key(current, key) }
157
+ end
158
+
159
+ def fetch_contract_key(value, key)
160
+ return value.fetch(key) if value.respond_to?(:key?) && value.key?(key)
161
+ return value.fetch(key.to_s) if value.respond_to?(:key?) && value.key?(key.to_s)
162
+ return fetch_symbol_contract_key(value, key) if key.respond_to?(:to_sym)
163
+
164
+ flunk("expected key #{key.inspect} in #{value.inspect}")
165
+ end
166
+
167
+ def fetch_symbol_contract_key(value, key)
168
+ return value.fetch(key.to_sym) if value.respond_to?(:key?) && value.key?(key.to_sym)
169
+
170
+ flunk("expected key #{key.inspect} in #{value.inspect}")
171
+ end
172
+
173
+ def contract_traceparent = "00-06796866738c859f2f19b7cfb3214824-000000000000004a-01"
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Contracts
7
+ module Wire
8
+ def assert_julewire_propagation_contract(key: Julewire::Core::Propagation::Carrier::DEFAULT_KEY)
9
+ carrier = {}
10
+ extracted = nil
11
+ restored = nil
12
+
13
+ Julewire.with_execution(type: :contract, id: "contract-1", emit_summary: false) do
14
+ Julewire.context.add(request_id: "request-1")
15
+ Julewire.carry.add(http: { request_headers: { traceparent: contract_traceparent } })
16
+
17
+ assert_same carrier, Julewire::Core::Propagation::Carrier.inject(carrier, key: key)
18
+
19
+ extracted = Julewire::Core::Propagation::Carrier.extract(carrier, key: key)
20
+ assert_equal "request-1", extracted.dig(:context, :request_id)
21
+ assert_equal contract_traceparent, extracted.dig(:carry, :http, :request_headers, :traceparent)
22
+ assert_equal "contract-1", extracted.dig(:execution, :id)
23
+
24
+ Julewire::Core::Propagation::Carrier.restore(carrier, key: key) do
25
+ restored = Julewire::Core::Propagation.capture_local
26
+ end
27
+ end
28
+
29
+ assert_equal "request-1", restored.dig(:context, :request_id)
30
+ assert_equal contract_traceparent, restored.dig(:carry, :http, :request_headers, :traceparent)
31
+ assert_equal "contract-1", restored.dig(:execution, :id)
32
+ assert_oversize_carrier_clears_stale_value!(key)
33
+
34
+ extracted
35
+ end
36
+
37
+ private
38
+
39
+ def assert_oversize_carrier_clears_stale_value!(key)
40
+ string_key = key.to_s
41
+ symbol_key = key.is_a?(String) ? key.to_sym : key
42
+ carrier = { string_key => "stale" }
43
+ carrier[symbol_key] = "stale" if symbol_key != string_key
44
+
45
+ result = Julewire::Core::Propagation::Carrier.inject(
46
+ carrier,
47
+ envelope: { context: { large: "x" * 64 } },
48
+ key: key,
49
+ max_bytes: 1
50
+ )
51
+
52
+ assert_nil result
53
+ refute carrier.key?(string_key)
54
+ refute carrier.key?(symbol_key) if symbol_key != string_key
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "contracts/component"
4
+ require_relative "contracts/deadline_scheduler"
5
+ require_relative "contracts/integration"
6
+ require_relative "contracts/record_draft"
7
+ require_relative "contracts/runtime"
8
+ require_relative "contracts/wire"
9
+
10
+ module Julewire
11
+ module Core
12
+ module Testing
13
+ # @api extension
14
+ module Contracts
15
+ include Component
16
+ include DeadlineScheduler
17
+ include Integration
18
+ include RecordDraft
19
+ include Runtime
20
+ include Wire
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ # @api extension
7
+ module Coverage
8
+ DEFAULT_MINIMUM_LINE = 96
9
+ DEFAULT_MINIMUM_BRANCH = 87
10
+
11
+ class << self
12
+ def start!(minimum_line: DEFAULT_MINIMUM_LINE, minimum_branch: DEFAULT_MINIMUM_BRANCH, filters: [])
13
+ return unless ENV["COVERAGE"]
14
+
15
+ require "simplecov"
16
+ require "simplecov-lcov"
17
+
18
+ configure_lcov_formatter
19
+ configure_formatters
20
+ start_simplecov(minimum_line: minimum_line, minimum_branch: minimum_branch, filters: filters)
21
+ nil
22
+ end
23
+
24
+ def configure_lcov_formatter
25
+ SimpleCov::Formatter::LcovFormatter.config do |config|
26
+ config.report_with_single_file = true
27
+ config.output_directory = "coverage/lcov"
28
+ config.lcov_file_name = "lcov.info"
29
+ config.single_report_path = "coverage/lcov/lcov.info"
30
+ end
31
+ end
32
+
33
+ def configure_formatters
34
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
35
+ [
36
+ SimpleCov::Formatter::HTMLFormatter,
37
+ SimpleCov::Formatter::LcovFormatter
38
+ ]
39
+ )
40
+ end
41
+
42
+ def start_simplecov(minimum_line:, minimum_branch:, filters:)
43
+ SimpleCov.start do
44
+ enable_coverage :branch
45
+ minimum_coverage line: minimum_line, branch: minimum_branch if minimum_line || minimum_branch
46
+ track_files "lib/**/*.rb"
47
+ add_filter "/test/"
48
+ add_filter "/lib/julewire/core/testing/coverage.rb"
49
+ add_filter %r{/lib/julewire/[^/]+/version\.rb\z}
50
+ add_filter %r{/lib/julewire-[^/]+\.rb\z}
51
+ filters.each { add_filter(it) }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module TestReports
7
+ module CodecovJUnitReportMethods
8
+ def report
9
+ return super unless @single_file
10
+
11
+ puts "Writing XML reports to #{reports_path}"
12
+ File.write(filename_for("minitest"), testsuites_xml)
13
+ end
14
+
15
+ private
16
+
17
+ def testsuites_xml
18
+ suites = tests.group_by { test_class(it) }
19
+ result = analyze_suite(tests)
20
+ xml = Builder::XmlMarkup.new(indent: 2)
21
+ xml.instruct!
22
+ xml.testsuites(testsuite_result_attributes(result)) do
23
+ suites.each do |suite, suite_tests|
24
+ parse_xml_for(xml, suite, suite_tests)
25
+ end
26
+ end
27
+
28
+ xml.target!
29
+ end
30
+
31
+ def testsuite_result_attributes(result)
32
+ {
33
+ name: "minitest",
34
+ skipped: result[:skip_count],
35
+ failures: result[:fail_count],
36
+ errors: result[:error_count],
37
+ tests: result[:test_count],
38
+ assertions: result[:assertion_count],
39
+ time: result[:time]
40
+ }
41
+ end
42
+ end
43
+ private_constant :CodecovJUnitReportMethods
44
+
45
+ module_function
46
+
47
+ def start!(enabled: ENV.fetch("JULEWIRE_JUNIT", nil),
48
+ reports_dir: ENV.fetch("JULEWIRE_JUNIT_DIR", "test/reports"))
49
+ return unless enabled
50
+
51
+ require "fileutils"
52
+ require "minitest/reporters"
53
+
54
+ FileUtils.mkdir_p(reports_dir)
55
+ junit = junit_reporter_class.new(
56
+ reports_dir,
57
+ true,
58
+ base_path: Dir.pwd,
59
+ include_timestamp: true,
60
+ single_file: true
61
+ )
62
+
63
+ Minitest::Reporters.use!(
64
+ [Minitest::Reporters::DefaultReporter.new, junit],
65
+ ENV,
66
+ Minitest.backtrace_filter
67
+ )
68
+ end
69
+
70
+ def junit_reporter_class
71
+ @junit_reporter_class ||= Class.new(Minitest::Reporters::JUnitReporter) do
72
+ include CodecovJUnitReportMethods
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ # @api extension
6
+ module Testing
7
+ # @api extension
8
+ class CaptureDestination
9
+ attr_reader :name, :records
10
+
11
+ def initialize(name: :capture, snapshot: true)
12
+ @name = name
13
+ @snapshot = snapshot
14
+ @records = []
15
+ end
16
+
17
+ def emit(record)
18
+ @records << (@snapshot ? record.to_h : record)
19
+ nil
20
+ end
21
+
22
+ def flush(*)
23
+ self
24
+ end
25
+
26
+ def close(*)
27
+ self
28
+ end
29
+
30
+ def health
31
+ { status: :ok, counts: { captured: @records.size } }
32
+ end
33
+
34
+ def clear
35
+ @records.clear
36
+ self
37
+ end
38
+ end
39
+
40
+ # @api extension
41
+ class NullOutput
42
+ attr_reader :writes
43
+
44
+ def initialize
45
+ @writes = []
46
+ end
47
+
48
+ def write(value)
49
+ @writes << value
50
+ value.bytesize
51
+ end
52
+
53
+ def flush = self
54
+
55
+ def close = self
56
+ end
57
+
58
+ class << self
59
+ def configure_capture_destination(runtime = Julewire, **)
60
+ destination = CaptureDestination.new(**)
61
+ runtime.configure do |config|
62
+ config.destinations.clear
63
+ config.destinations.add(destination)
64
+ end
65
+ destination
66
+ end
67
+
68
+ def capture(runtime = Julewire, **)
69
+ records = configure_capture_destination(runtime, **).records
70
+ yield records if block_given?
71
+ records
72
+ end
73
+
74
+ def unregister_destination(kind)
75
+ Core::Destinations.__send__(:unregister, kind)
76
+ end
77
+
78
+ def reset_shared_scheduler
79
+ Core::Scheduling::SharedScheduler.__send__(:reset_for_test!)
80
+ end
81
+
82
+ def with_overridden_singleton_method(receiver, method_name, replacement)
83
+ singleton_class = class << receiver; self; end
84
+ method_exists =
85
+ singleton_class.method_defined?(method_name) || singleton_class.private_method_defined?(method_name)
86
+ original = singleton_class.instance_method(method_name) if method_exists
87
+ verbose = $VERBOSE
88
+ $VERBOSE = nil
89
+ singleton_class.define_method(method_name, replacement)
90
+ yield
91
+ ensure
92
+ $VERBOSE = nil
93
+ if original
94
+ singleton_class.define_method(method_name, original)
95
+ elsif singleton_class&.method_defined?(method_name) || singleton_class&.private_method_defined?(method_name)
96
+ singleton_class.remove_method(method_name)
97
+ end
98
+ $VERBOSE = verbose
99
+ end
100
+
101
+ def nonblocking_queue_values(queue)
102
+ values = []
103
+ loop do
104
+ values << queue.pop(true)
105
+ rescue ThreadError
106
+ return values
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ Testing = Core::Testing unless const_defined?(:Testing, false)
114
+ end
115
+
116
+ require_relative "testing/chaos"
117
+ require_relative "testing/chaos/catalog"
118
+ require_relative "testing/chaos/core_runtime"
119
+ require_relative "testing/chaos/destination"
120
+ require_relative "testing/chaos/emitter"
121
+ require_relative "testing/chaos/raising_output"
122
+ require_relative "testing/contracts"
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ # @api integration_spi
6
+ module Validation
7
+ class << self
8
+ def validate_byte_limit!(value, name:)
9
+ return if value.nil?
10
+
11
+ validate_integer_limit!(value, name: name, positive: true)
12
+ rescue ArgumentError
13
+ raise ArgumentError, "#{name} must be nil or a positive Integer"
14
+ end
15
+
16
+ def validate_non_negative_integer!(value, name:)
17
+ validate_integer_limit!(value, name: name)
18
+ end
19
+
20
+ def validate_integer_limit!(value, name:, positive: false)
21
+ return value if value.is_a?(Integer) && valid_integer_limit?(value, positive: positive)
22
+
23
+ qualifier = positive ? "positive" : "non-negative"
24
+ raise ArgumentError, "#{name} must be a #{qualifier} Integer"
25
+ end
26
+
27
+ def validate_callable!(value, name:, allow_nil: false)
28
+ return if allow_nil && value.nil?
29
+ return if value.respond_to?(:call)
30
+
31
+ raise ArgumentError, "#{name} must respond to #call"
32
+ end
33
+
34
+ def validate_options!(options, allowed_keys, name:)
35
+ unknown_options = options.keys - allowed_keys
36
+ return if unknown_options.empty?
37
+
38
+ raise ArgumentError, "unknown #{name} options: #{unknown_options.join(", ")}"
39
+ end
40
+
41
+ def validate_symbol_choice!(value, name:, choices:)
42
+ choice = value.to_sym if value.respond_to?(:to_sym)
43
+ return choice if choices.include?(choice)
44
+
45
+ raise ArgumentError, "#{name} must be one of: #{choices.join(", ")}"
46
+ end
47
+
48
+ def validate_timeout!(timeout, name:)
49
+ return if timeout.nil?
50
+ return if valid_numeric_timeout?(timeout)
51
+
52
+ raise ArgumentError, "#{name} must be nil or a non-negative finite Numeric"
53
+ end
54
+
55
+ def valid_numeric_timeout?(timeout)
56
+ timeout.is_a?(Numeric) && timeout.finite? && timeout >= 0
57
+ rescue StandardError
58
+ false
59
+ end
60
+
61
+ def valid_integer_limit?(value, positive:)
62
+ positive ? value.positive? : value >= 0
63
+ end
64
+
65
+ private :valid_integer_limit?, :valid_numeric_timeout?
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ module Julewire
6
+ # Each gem extends the shared Julewire namespace.
7
+ loader = Zeitwerk::Loader.for_gem_extension(self)
8
+ loader.inflector.inflect("cli" => "CLI")
9
+ loader.ignore("#{__dir__}/core/testing.rb", "#{__dir__}/core/testing")
10
+ loader.setup
11
+
12
+ module Core
13
+ DEFAULT_MAX_RECORD_BYTES = 1_048_576
14
+ MAX_BACKTRACE_LINES = 20
15
+ NORMALIZATION_MAX_DEPTH = 128
16
+ CIRCULAR_REFERENCE = "[Circular]"
17
+
18
+ class << self
19
+ def sentinel(name) = Sentinel.new(name)
20
+
21
+ def normalize_name(value, name: :name)
22
+ case value
23
+ when String
24
+ raise ArgumentError, "#{name} must not be empty" if value.empty?
25
+
26
+ value.to_sym
27
+ when Symbol
28
+ raise ArgumentError, "#{name} must not be empty" if value.name.empty?
29
+
30
+ value
31
+ else
32
+ raise ArgumentError, "#{name} must be a String or Symbol"
33
+ end
34
+ end
35
+
36
+ def deep_compact_empty(value)
37
+ Serialization::DeepCompactEmpty.call(value)
38
+ end
39
+
40
+ def emit_input(input, fields)
41
+ return fields if input.equal?(UNSET)
42
+ return input if fields.empty?
43
+ return input.merge(fields) if input.is_a?(Hash)
44
+
45
+ { message: input.to_s }.merge(fields)
46
+ end
47
+ end
48
+
49
+ UNSET = sentinel(:unset)
50
+ MISSING = sentinel(:missing)
51
+ private_constant :MISSING
52
+ end
53
+
54
+ extend Core::FacadeMethods
55
+
56
+ Core::RuntimeLocator.current = Core::Runtime.new
57
+ Core.singleton_class.class_eval do
58
+ define_method(:loader) { loader }
59
+ private :loader
60
+ end
61
+
62
+ ConsoleFormatter = Core::Records::ConsoleFormatter
63
+ JsonEncoder = Core::Serialization::JsonEncoder
64
+ Match = Core::Processing::Match
65
+ Record = Core::Records::Record
66
+ RecordDraft = Core::Records::Draft
67
+ RecordFormatter = Core::Records::Formatter
68
+ Sampling = Core::Processing::Sampling
69
+ Serializer = Core::Serialization::Serializer
70
+ Tail = Core::Diagnostics::Tail
71
+ TailSampling = Core::Destinations::TailSampling
72
+ TextEncoder = Core::Serialization::TextEncoder
73
+
74
+ Core::Processing.register(:sampling) do |rate:, key: nil|
75
+ Core::Processing::Sampling.head(rate: rate, key: key)
76
+ end
77
+ Core::Destinations.register(:tail_sampling) do |name:, **options|
78
+ Core::Destinations::TailSampling.new(name: name, **options)
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "julewire/core"