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,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
|