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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- 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,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
|