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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Chaos
7
+ module Destination
8
+ class << self
9
+ def assert_contract(test_context, record:, formatter:, encoder:, output:, callbacks:, errors:)
10
+ {
11
+ formatter: formatter,
12
+ encoder: encoder,
13
+ output: output,
14
+ callbacks: callbacks
15
+ }.compact.each do |scenario, builder|
16
+ assert_scenario(test_context, scenario, builder, record, errors)
17
+ end
18
+ nil
19
+ end
20
+
21
+ private
22
+
23
+ def assert_scenario(test_context, scenario, builder, record, errors)
24
+ errors.each do |error|
25
+ assert_error_contained(test_context, scenario, builder, record, error)
26
+ end
27
+ nil
28
+ end
29
+
30
+ def assert_error_contained(test_context, scenario, builder, record, error)
31
+ Chaos.assert_contained(
32
+ test_context,
33
+ errors: [error],
34
+ description: "destination #{scenario}"
35
+ ) do |build_error|
36
+ destination = builder.call(build_error)
37
+ destination.emit(record)
38
+ ensure
39
+ close_destination(destination)
40
+ end
41
+ end
42
+
43
+ def close_destination(destination)
44
+ return unless destination.respond_to?(:close)
45
+
46
+ destination.close(timeout: 0)
47
+ rescue StandardError
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Chaos
7
+ module Emitter
8
+ class << self
9
+ def assert_contract(test_context, component:, build:, exercise:, errors:)
10
+ Chaos.assert_contained(test_context, errors: errors, description: component) do |error|
11
+ emitter = build.call(error)
12
+ exercise.call(emitter, error)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Chaos
7
+ class RaisingOutput
8
+ def initialize(error, failures:)
9
+ @error = error
10
+ @failures = failures
11
+ end
12
+
13
+ def write(value)
14
+ raise @error if @failures.include?(:write)
15
+
16
+ value.bytesize
17
+ end
18
+
19
+ def flush
20
+ raise @error if @failures.include?(:flush)
21
+
22
+ self
23
+ end
24
+
25
+ def close
26
+ raise @error if @failures.include?(:close)
27
+
28
+ self
29
+ end
30
+
31
+ def after_fork!
32
+ raise @error if @failures.include?(:after_fork)
33
+
34
+ self
35
+ end
36
+ end
37
+
38
+ private_constant :RaisingOutput
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ # @api extension
7
+ module Chaos
8
+ DEFAULT_ERRORS = [
9
+ RuntimeError.new("julewire chaos runtime"),
10
+ ArgumentError.new("julewire chaos argument"),
11
+ TypeError.new("julewire chaos type")
12
+ ].freeze
13
+ class << self
14
+ def assert_contained(test_context, errors: DEFAULT_ERRORS, description: nil)
15
+ raise ArgumentError, "block required" unless block_given?
16
+
17
+ errors.each do |error|
18
+ yield error
19
+ rescue StandardError => e
20
+ test_context.flunk(containment_message(description, error, e))
21
+ end
22
+ nil
23
+ end
24
+
25
+ def assert_core_runtime_containment(test_context, runtime: Julewire, reset: nil, errors: DEFAULT_ERRORS)
26
+ reset ||= -> { runtime.reset! }
27
+ raise ArgumentError, "reset must respond to call" unless reset.respond_to?(:call)
28
+
29
+ CoreRuntime.assert_contract(test_context, runtime: runtime, reset: reset, errors: errors)
30
+ end
31
+
32
+ def catalog(&)
33
+ Catalog.build(&)
34
+ end
35
+
36
+ def assert_discovered_chaos_contracts(test_context, catalog:, errors: DEFAULT_ERRORS)
37
+ Catalog.assert_contract(test_context, catalog: catalog, errors: errors)
38
+ end
39
+
40
+ def assert_destination_chaos_contract(test_context, record:, formatter:, encoder:, output:,
41
+ callbacks: nil, errors: DEFAULT_ERRORS)
42
+ Destination.assert_contract(
43
+ test_context,
44
+ record: record,
45
+ formatter: formatter,
46
+ encoder: encoder,
47
+ output: output,
48
+ callbacks: callbacks,
49
+ errors: errors
50
+ )
51
+ end
52
+
53
+ def assert_emitter_chaos_contract(test_context, component:, build:, exercise:, errors: DEFAULT_ERRORS)
54
+ Emitter.assert_contract(
55
+ test_context,
56
+ component: component,
57
+ build: build,
58
+ exercise: exercise,
59
+ errors: errors
60
+ )
61
+ end
62
+
63
+ def raiser(error = RuntimeError.new("julewire chaos"))
64
+ ->(*) { raise error }
65
+ end
66
+
67
+ private
68
+
69
+ def containment_message(description, expected_error, leaked_error)
70
+ unless description
71
+ return "expected #{expected_error.class} to be contained, leaked #{leaked_error.class}: #{leaked_error}"
72
+ end
73
+
74
+ "expected #{description} chaos to be contained, leaked #{leaked_error.class}: #{leaked_error}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Julewire
6
+ module Core
7
+ module Testing
8
+ module Contracts
9
+ module Component
10
+ def assert_julewire_processor_contract(processor, draft: build_julewire_contract_draft)
11
+ assert_respond_to processor, :call
12
+
13
+ result = processor.call(draft)
14
+ return :drop if result == :drop
15
+
16
+ result = draft unless result.is_a?(Julewire::Core::Records::Draft)
17
+ result.to_record
18
+ result
19
+ end
20
+
21
+ def assert_julewire_formatter_contract(formatter, record: build_julewire_contract_record)
22
+ assert_respond_to formatter, :call
23
+
24
+ formatted = formatter.call(record)
25
+ encoded = Julewire::Core::Serialization::JsonEncoder.new.call(formatted)
26
+
27
+ assert_kind_of String, encoded
28
+ formatted
29
+ end
30
+
31
+ def assert_julewire_destination_contract(destination, record: build_julewire_contract_record)
32
+ %i[name emit flush close health].each do |method_name|
33
+ assert_respond_to destination, method_name
34
+ end
35
+
36
+ assert_nil destination.emit(record)
37
+ assert destination.flush(timeout: 0)
38
+ assert destination.close(timeout: 0)
39
+ assert_kind_of Hash, destination.health
40
+ destination
41
+ end
42
+
43
+ def assert_julewire_record_shape_contract(record: build_julewire_shape_contract_record)
44
+ Julewire::Core::Records::Record.validate_normalized!(record)
45
+
46
+ data = record.to_h
47
+ assert_julewire_record_data_shape!(record, data)
48
+ assert_julewire_record_formatter_shape!(record)
49
+ assert_julewire_record_serializer_shape!(record)
50
+
51
+ record
52
+ end
53
+
54
+ def build_julewire_contract_record(fields = {})
55
+ build_julewire_contract_draft(fields).to_record
56
+ end
57
+
58
+ def build_julewire_contract_draft(fields = {})
59
+ Julewire::Core::Records::Draft.build(
60
+ {
61
+ severity: :info,
62
+ kind: :point,
63
+ event: "test.event",
64
+ source: "test",
65
+ message: "test message",
66
+ attributes: { "test.attribute" => "value" },
67
+ payload: { value: 1 }
68
+ }.merge(fields),
69
+ context: {},
70
+ scope: nil,
71
+ freeze_sections: false
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ def assert_julewire_record_data_shape!(record, data)
78
+ assert_julewire_symbol_keys!(data)
79
+ assert_equal(
80
+ Julewire::Core::Records::Record::REQUIRED_KEYS,
81
+ data.keys & Julewire::Core::Records::Record::REQUIRED_KEYS
82
+ )
83
+ assert record.frozen?
84
+ assert record.serializable_data.frozen?
85
+ refute data.fetch(:execution).key?(:ancestors)
86
+ refute data.fetch(:execution).key?(:ancestors_truncated)
87
+ assert_equal [{ type: :request, id: "root-1" }], record.lineage.ancestors
88
+ assert_equal "root-1", record.lineage.root_reference[:id]
89
+ end
90
+
91
+ def assert_julewire_record_formatter_shape!(record)
92
+ formatted = Julewire::Core::Serialization::Serializer.call(
93
+ Julewire::Core::Records::Formatter.new.call(record),
94
+ compact_empty: true
95
+ )
96
+ refute formatted.key?("carry")
97
+ assert_equal "visible", formatted.dig("execution", "custom")
98
+ %w[root parent ancestors depth ancestors_truncated].each do |key|
99
+ refute formatted.fetch("execution").key?(key)
100
+ end
101
+ end
102
+
103
+ def assert_julewire_record_serializer_shape!(record)
104
+ serialized = Julewire::Core::Serialization::Serializer.call(record)
105
+ assert_equal(
106
+ Julewire::Core::Serialization::ValueCopy::CIRCULAR_REFERENCE,
107
+ serialized.dig("payload", "cycle", "self")
108
+ )
109
+ assert_equal Julewire::Core::Serialization::Serializer::NAN_VALUE, serialized.dig("payload", "nan")
110
+ assert_equal "1.25", serialized.dig("payload", "decimal")
111
+ JSON.generate(serialized, allow_nan: false)
112
+ end
113
+
114
+ def build_julewire_shape_contract_record
115
+ cycle = {}
116
+ cycle[:self] = cycle
117
+
118
+ build_julewire_contract_record(
119
+ execution: {
120
+ type: :request,
121
+ id: "execution-1",
122
+ root: { type: :request, id: "root-1" },
123
+ parent: { type: :job, id: "parent-1" },
124
+ ancestors: [{ type: :request, id: "root-1" }],
125
+ ancestors_truncated: false,
126
+ depth: 2,
127
+ custom: "visible"
128
+ },
129
+ context: { request_id: "request-1" },
130
+ neutral: Fields::AttributeKeys.fields("http.request.method" => "GET"),
131
+ carry: { http: { request_headers: { traceparent: contract_traceparent } } },
132
+ payload: {
133
+ value: 1,
134
+ nested: { string_key: true },
135
+ cycle: cycle,
136
+ nan: Float::NAN,
137
+ decimal: contract_decimal
138
+ }
139
+ )
140
+ end
141
+
142
+ def contract_decimal
143
+ defined?(BigDecimal) ? BigDecimal("1.25") : "1.25"
144
+ end
145
+
146
+ def assert_julewire_symbol_keys!(value)
147
+ case value
148
+ when Hash
149
+ assert(
150
+ value.keys.all?(Symbol),
151
+ "expected only symbol keys in #{value.inspect}"
152
+ )
153
+ value.each_value { assert_julewire_symbol_keys!(it) }
154
+ when Array
155
+ value.each { assert_julewire_symbol_keys!(it) }
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module Julewire
6
+ module Core
7
+ module Testing
8
+ module Contracts
9
+ module DeadlineScheduler
10
+ def assert_julewire_deadline_scheduler_spi_contract
11
+ scheduler = Julewire::Core::Scheduling::DeadlineScheduler.new(thread_name: "julewire-contract-deadline")
12
+ called = false
13
+
14
+ result = scheduler.schedule(0) { called = true }
15
+
16
+ assert called
17
+ assert_nil result
18
+ assert_nil scheduler.cancel(nil)
19
+ assert_scheduler_runs_callbacks(scheduler)
20
+ assert_scheduler_cancel_suppresses_callback(scheduler)
21
+ assert_scheduler_after_fork_resets_pending_callbacks(scheduler)
22
+ end
23
+
24
+ private
25
+
26
+ def assert_scheduler_runs_callbacks(scheduler)
27
+ queue = Queue.new
28
+
29
+ scheduler.schedule(0.001) { queue << :done }
30
+
31
+ assert_equal :done, Timeout.timeout(1) { queue.pop }
32
+ end
33
+
34
+ def assert_scheduler_cancel_suppresses_callback(scheduler)
35
+ queue = Queue.new
36
+ token = scheduler.schedule(0.01) { queue << :cancelled }
37
+
38
+ scheduler.cancel(token)
39
+ scheduler.schedule(0.02) { queue << :sentinel }
40
+
41
+ assert_equal :sentinel, Timeout.timeout(1) { queue.pop }
42
+ assert_empty Julewire::Core::Testing.nonblocking_queue_values(queue)
43
+ end
44
+
45
+ def assert_scheduler_after_fork_resets_pending_callbacks(scheduler)
46
+ queue = Queue.new
47
+
48
+ scheduler.schedule(0.01) { queue << :old }
49
+ assert_same scheduler, scheduler.after_fork!
50
+ scheduler.schedule(0.001) { queue << :new }
51
+
52
+ assert_equal :new, Timeout.timeout(1) { queue.pop }
53
+ assert_empty Julewire::Core::Testing.nonblocking_queue_values(queue)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "integration_fields"
4
+
5
+ module Julewire
6
+ module Core
7
+ module Testing
8
+ module Contracts
9
+ module Integration
10
+ include IntegrationFields
11
+
12
+ def assert_julewire_integration_spi_contract
13
+ assert_julewire_integration_health_contract
14
+ assert_julewire_integration_field_overlay_contract
15
+ assert_julewire_integration_timestamp_contract
16
+ assert_julewire_integration_payload_contract
17
+ assert_julewire_integration_value_contract
18
+ assert_julewire_deadline_scheduler_spi_contract
19
+ assert_julewire_integration_ivar_state_contract
20
+ end
21
+
22
+ def assert_julewire_validation_spi_contract
23
+ validation = Julewire::Core::Validation
24
+ assert_nil validation.validate_options!({ known: true }, %i[known], name: :contract)
25
+ assert_equal 1, validation.validate_byte_limit!(1, name: :limit)
26
+ assert_equal 0, validation.validate_integer_limit!(0, name: :count)
27
+
28
+ error = assert_raises(ArgumentError) do
29
+ validation.validate_options!({ unknown: true }, %i[known], name: :contract)
30
+ end
31
+ assert_match "unknown contract options: unknown", error.message
32
+
33
+ error = assert_raises(ArgumentError) do
34
+ validation.validate_byte_limit!(0, name: :limit)
35
+ end
36
+ assert_match "limit must be nil or a positive Integer", error.message
37
+
38
+ error = assert_raises(ArgumentError) do
39
+ validation.validate_integer_limit!(-1, name: :count)
40
+ end
41
+ assert_match "count must be a non-negative Integer", error.message
42
+ end
43
+
44
+ def assert_julewire_truncation_marker_spi_contract
45
+ assert_equal "[MaxDepth]", Julewire::Core::Serialization::Serializer::MAX_DEPTH_VALUE
46
+ assert_equal "...[Truncated]", Julewire::Core::Serialization::Serializer::TRUNCATED_SUFFIX
47
+ assert_equal "_julewire_truncation", Julewire::Core::Serialization::Serializer::TRUNCATION_METADATA_KEY
48
+ assert_equal(
49
+ {
50
+ "truncated" => true,
51
+ "truncated_fields" => ["array_items"],
52
+ "limits" => {
53
+ "max_array_items" => 1,
54
+ "max_depth" => 8,
55
+ "max_hash_keys" => 1_000,
56
+ "max_string_bytes" => 3
57
+ }
58
+ },
59
+ Julewire::Core::Serialization::Serializer.truncation_metadata(
60
+ ["array_items"],
61
+ max_array_items: 1,
62
+ max_string_bytes: 3
63
+ )
64
+ )
65
+ end
66
+
67
+ def assert_julewire_bounded_transform_spi_contract
68
+ marker_key = Julewire::Core::Serialization::Serializer::TRUNCATION_METADATA_KEY.to_sym
69
+ result = Julewire::Core::Serialization::BoundedTransform.call(
70
+ { secret: "value", list: [1, 2], long: "abcdef" },
71
+ max_array_items: 1,
72
+ max_string_bytes: 3
73
+ ) do |_value, key:, **|
74
+ key == :secret ? "[FILTERED]" : Julewire::Core::Serialization::BoundedTransform::CONTINUE
75
+ end
76
+
77
+ assert_equal "[FI...[Truncated]", result.fetch(:secret)
78
+ assert_equal "abc...[Truncated]", result.fetch(:long)
79
+ assert_equal ["array_items"], result.dig(:list, 1, marker_key, "truncated_fields")
80
+ end
81
+
82
+ def assert_julewire_integration_failure_contract(integration:, component:, exercise:)
83
+ assert_nil exercise.call
84
+
85
+ health = Julewire.health
86
+ integration_health = health.dig(:process_integrations, integration.to_sym)
87
+
88
+ assert_equal :degraded, health.fetch(:status)
89
+ assert_kind_of Hash, integration_health
90
+ assert_equal :degraded, integration_health.fetch(:status)
91
+ assert_equal 1, integration_health.dig(:counts, :failures)
92
+ assert_equal component.to_sym, integration_health.dig(:last_failure, :component)
93
+ refute_includes integration_health.fetch(:last_failure), :message
94
+
95
+ [health, integration_health]
96
+ end
97
+
98
+ def assert_julewire_integration_health_contract
99
+ Julewire::Core::Diagnostics::ProcessIntegrationHealth.reset!
100
+ Julewire::Core::Integration::Health.record_failure(
101
+ :contract,
102
+ RuntimeError.new("secret"),
103
+ component: :subscriber
104
+ )
105
+
106
+ degraded = Julewire::Core::Diagnostics::ProcessIntegrationHealth.health.fetch(:contract)
107
+ assert_equal :degraded, degraded.fetch(:status)
108
+ assert_equal 1, degraded.dig(:counts, :failures)
109
+ refute_includes degraded.fetch(:last_failure), :message
110
+
111
+ Julewire::Core::Integration::Health.record_success(:contract)
112
+ recovered = Julewire::Core::Diagnostics::ProcessIntegrationHealth.health.fetch(:contract)
113
+ assert_equal :ok, recovered.fetch(:status)
114
+ assert_equal 1, recovered.dig(:counts, :failures)
115
+ assert_equal "RuntimeError", recovered.dig(:last_failure, :class)
116
+ ensure
117
+ Julewire::Core::Diagnostics::ProcessIntegrationHealth.reset!
118
+ end
119
+
120
+ def assert_julewire_integration_timestamp_contract
121
+ now = Time.utc(2026, 5, 30, 12, 0, 0, 123_456)
122
+ values = Julewire::Core::Integration::Values::Shape
123
+
124
+ assert_equal "2026-05-30T12:00:00.123456000Z", values.timestamp(now)
125
+ assert_equal "1970-01-01T00:00:01.000000002Z", values.timestamp(1_000_000_002)
126
+ end
127
+
128
+ def assert_julewire_integration_payload_contract
129
+ values = Julewire::Core::Integration::Values::Shape
130
+
131
+ assert_equal({ account_id: "acct-1" }, values.payload_hash("account_id" => "acct-1"))
132
+ assert_equal(
133
+ { Julewire::Core::Fields::FieldSet::VALUE_KEY => "raw" },
134
+ values.payload_hash("raw")
135
+ )
136
+ assert_equal({ request_id: "req-1" }, values.hash_or_empty("request_id" => "req-1"))
137
+ assert_equal({}, values.hash_or_empty("raw"))
138
+ end
139
+
140
+ def assert_julewire_integration_value_contract
141
+ values = Julewire::Core::Integration::Values::Read
142
+
143
+ assert_equal [true, false], [values.blank?(""), values.blank?("value")]
144
+ assert_equal "symbol", values.value({ key: "symbol" }, :key)
145
+ assert_equal "string", values.value({ "key" => "string" }, :key)
146
+ assert_equal "method", values.value(Class.new { def key = "method" }.new, :key)
147
+ assert_equal(
148
+ "nested",
149
+ values.nested_value({ outer: { "inner" => "nested" } }, :outer, :inner)
150
+ )
151
+ assert_equal "path", values.path_value({ outer: { "inner" => "path" } }, %i[outer inner])
152
+ assert_equal :fallback, values.value(Object.new, :missing, default: :fallback)
153
+ end
154
+
155
+ def assert_julewire_integration_ivar_state_contract
156
+ owner = Object.new
157
+ state = Julewire::Core::Integration::IvarState.new(:@julewire_contract_install)
158
+ assert_nil state.fetch(owner)
159
+ assert_equal :installed, state.store(owner, :installed)
160
+ assert_equal :installed, state.fetch(owner)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Contracts
7
+ module IntegrationFields
8
+ private
9
+
10
+ def assert_julewire_integration_field_overlay_contract
11
+ records = Julewire::Core::Testing.capture(snapshot: true) do
12
+ Julewire.with_execution(type: :contract, emit_summary: false) do
13
+ Julewire::Core::Integration::Facade.add_context(contract_context: "ctx")
14
+ Julewire::Core::Integration::Facade.add_carry(contract_carry: "carry")
15
+ Julewire::Core::Integration::Facade.add_attributes(contract_attribute: "attr")
16
+ Julewire::Core::Integration::Facade.add_neutral("contract.neutral": "neutral")
17
+ Julewire.emit(event: "contract.spi", source: "contract")
18
+ end
19
+ Julewire.flush
20
+ end
21
+
22
+ record = records.find { it[:event] == "contract.spi" }
23
+ flunk("expected integration field overlay contract record") unless record
24
+
25
+ assert_equal "ctx", record.dig(:context, :contract_context)
26
+ assert_equal "carry", record.dig(:carry, :contract_carry)
27
+ assert_equal "attr", record.dig(:attributes, :contract_attribute)
28
+ assert_equal "neutral", record.dig(:neutral, :"contract.neutral")
29
+ ensure
30
+ Julewire.reset!
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Testing
6
+ module Contracts
7
+ module RecordDraft
8
+ def assert_julewire_record_draft_transform_contract(draft: build_julewire_transform_contract_draft)
9
+ draft.transform_field!(:message) { |message| "#{message} transformed" }
10
+ draft.transform_section!(:payload) { it.merge(section_transformed: true) }
11
+ draft.transform_record! do |data|
12
+ data.merge(labels: data.fetch(:labels).merge(record_transformed: "yes"))
13
+ end
14
+
15
+ record = draft.to_record
16
+ assert_equal "test message transformed", record.fetch(:message)
17
+ assert record.dig(:payload, :section_transformed)
18
+ assert_equal "yes", record.dig(:labels, :record_transformed)
19
+ assert_equal [{ type: "request", id: "root-1" }], record.lineage.ancestors
20
+ draft
21
+ end
22
+
23
+ def build_julewire_transform_contract_draft
24
+ build_julewire_contract_draft(
25
+ execution: {
26
+ type: "job",
27
+ id: "job-1",
28
+ ancestors: [{ type: "request", id: "root-1" }]
29
+ },
30
+ labels: { service: "contract" }
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end